Getting Started with silvery/runtime
This is the "start simple" part — five lines to a working app. silvery/runtime is a layered TUI framework built on React. Write terminal apps the same way you write web apps — with components, hooks, and state. When you outgrow useState, Building an App shows how to graduate without rewriting. The core idea: each level you adopt turns something that was hidden — state transitions, side effects, user intent — into visible, inspectable data. You only pay for what you need.
Your First App
Create a terminal with createTerm(), then pass it to run():
import { createTerm } from 'silvery'
import { run, useInput } from '@silvery/term/runtime'
import { Text } from '@silvery/term'
import { useState } from 'react'
function Counter() {
const [count, setCount] = useState(0)
useInput((input, key) => {
if (input === 'j' || key.downArrow) setCount(c => c + 1)
if (input === 'k' || key.upArrow) setCount(c => c - 1)
if (input === 'q') return 'exit'
})
return <Text>Count: {count} (j/k to change, q to quit)</Text>
}
using term = createTerm()
await run(<Counter />, term)Run it and you'll see:
Count: 0 (j/k to change, q to quit)Press j a few times:
Count: 3 (j/k to change, q to quit)That's a complete, working TUI app. run() handles terminal setup, keyboard input, rendering, and cleanup. You write React components.
run() auto-detects your terminal and enables the best available features:
- Kitty keyboard protocol — enabled on Ghostty, Kitty, WezTerm, foot (Cmd ⌘ and Hyper ✦ modifiers, unambiguous key IDs)
- Mouse tracking — click, scroll, and drag events (set
mouse: falseto restore native copy/paste) - Focus reporting — terminal focus/blur events
- Text sizing — correct width for nerdfont/powerline icons (Kitty 0.40+, Ghostty)
- Alternate screen — clean slate, restored on exit
See Terminal Capabilities for the full protocol reference.
Building with run()
Keyboard Input
Use useInput to handle keyboard events. Return 'exit' from the handler to quit the app.
import { run, useInput, type Key } from "@silvery/term/runtime"
useInput((input: string, key: Key) => {
// Regular characters
if (input === "a") doSomething()
// Special keys
if (key.return) submit()
if (key.escape) cancel()
if (key.tab) nextField()
// Arrow keys
if (key.upArrow) moveCursor(-1)
if (key.downArrow) moveCursor(1)
// Modifiers
if (key.ctrl && input === "c") return "exit"
// Text input
if (input.length === 1) addChar(input)
})The Key object provides booleans for special keys and modifiers:
interface Key {
// Navigation
upArrow: boolean
downArrow: boolean
leftArrow: boolean
rightArrow: boolean
pageDown: boolean
pageUp: boolean
home: boolean
end: boolean
// Action keys
return: boolean // Enter key
escape: boolean
tab: boolean
backspace: boolean
delete: boolean
// Modifiers
ctrl: boolean // ⌃ Ctrl
shift: boolean // ⇧ Shift
meta: boolean // ⌥ Opt/Alt
super: boolean // ⌘ Cmd/Super (requires Kitty protocol)
hyper: boolean // ✦ Hyper (requires Kitty protocol)
// Kitty protocol extensions
eventType?: 1 | 2 | 3 // 1=press, 2=repeat, 3=release (requires REPORT_EVENTS)
}Wrap handlers in useCallback when they depend on state to prevent unnecessary re-subscriptions:
const handleInput = useCallback(
(input: string, key: Key) => {
if (input === "j" || key.downArrow) setCursor((c) => Math.min(c + 1, items.length - 1))
if (input === "k" || key.upArrow) setCursor((c) => Math.max(c - 1, 0))
if (input === "q") return "exit"
},
[items.length],
)
useInput(handleInput)Responsive Layout
Components can know their size during render:
import { useContentRect } from '@silvery/term'
function ResponsivePanel() {
const { width, height } = useContentRect()
return (
<Box flexDirection="column">
<Text>Panel is {width}x{height}</Text>
{height > 10 && <Text>Extra content when tall enough</Text>}
</Box>
)
}Terminal Capabilities
Access terminal info and styling with useTerm:
import { useTerm } from '@silvery/term'
function StatusLine() {
const term = useTerm()
return (
<Text>
{term.hasColor() ? term.green('OK') : 'OK'}
{` ${term.cols}x${term.rows}`}
</Text>
)
}A Complete Example: Interactive List
Putting hooks together into a real app:
import { run, useInput, useExit, type Key } from '@silvery/term/runtime'
import { Box, Text, useContentRect } from '@silvery/term'
import { useState, useCallback } from 'react'
function App() {
const [items, setItems] = useState(['Apple', 'Banana', 'Cherry'])
const [cursor, setCursor] = useState(0)
const { width } = useContentRect()
const handleInput = useCallback((input: string, key: Key) => {
if (input === 'j' || key.downArrow) setCursor(c => Math.min(c + 1, items.length - 1))
if (input === 'k' || key.upArrow) setCursor(c => Math.max(c - 1, 0))
if (input === 'q') return 'exit'
}, [items.length])
useInput(handleInput)
return (
<Box flexDirection="column">
<Text bold>{'─'.repeat(width)}</Text>
{items.map((item, i) => (
<Text key={item} color={i === cursor ? 'cyan' : undefined}>
{i === cursor ? '› ' : ' '}{item}
</Text>
))}
</Box>
)
}
const handle = await run(<App />)
await handle.waitUntilExit()RunHandle API
run() returns a handle for programmatic control:
interface RunHandle {
text: string // Current rendered text (no ANSI)
waitUntilExit(): Promise<void>
unmount(): void
press(key: string): Promise<void> // For testing
}Advanced Input: Kitty Protocol and Mouse
Kitty keyboard protocol and mouse tracking are enabled by default — run() auto-detects your terminal. Cmd ⌘ and Hyper ✦ modifiers work out of the box on supported terminals (Ghostty, Kitty, WezTerm, foot):
import { run, useInput } from "@silvery/term/runtime"
function App() {
useInput((input, key) => {
if (key.super && input === "s") save() // ⌘S
if (key.super && key.shift && input === "p") { // ⌘⇧P
openCommandPalette()
}
if (input === "q") return "exit"
})
return <Text>Press ⌘S to save</Text>
}
await run(<App />)To opt out of specific features:
await run(<App />, {
mouse: false, // Restore native terminal copy/paste
kitty: false, // Disable Kitty protocol (legacy ANSI only)
})For advanced Kitty flags (key release events, associated text):
import { KittyFlags } from "@silvery/term"
await run(<App />, {
kitty: KittyFlags.DISAMBIGUATE | KittyFlags.REPORT_EVENTS,
})See Input Features for the full reference.
Browser Rendering (xterm.js)
For browser rendering via xterm.js, use renderToXterm() from @silvery/term/xterm:
import { renderToXterm, Box, Text, useContentRect } from "@silvery/term/xterm"
import { Terminal } from "@xterm/xterm"
function App() {
const { width, height } = useContentRect()
return (
<Box flexDirection="column" padding={1}>
<Text bold>Browser App</Text>
<Text>
Terminal size: {width} x {height}
</Text>
</Box>
)
}
const term = new Terminal({ cols: 80, rows: 24 })
term.open(document.getElementById("terminal")!)
const instance = renderToXterm(<App />, term)Key differences from Node.js run():
- No runtime hooks --
renderToXterm()is a lightweight renderer withoutuseInput, focus management, or event loop - Resize handling -- Resize the terminal externally (e.g., via
FitAddon) and the app re-renders automatically - Input handling -- Use the
onKeyandonMouseoptions for keyboard/mouse input
Testing
All layers support testing without a real terminal:
// run()
const handle = await run(<Counter />, { cols: 80, rows: 24 })
expect(handle.text).toContain('Count: 0')
await handle.press('j')
expect(handle.text).toContain('Count: 1')
handle.unmount()
// createApp()
const handle = await app.run(<App />, { cols: 80, rows: 24 })
expect(handle.store.getState().count).toBe(0)
await handle.press('j')
expect(handle.store.getState().count).toBe(1)
handle.unmount()Migration from Ink
silvery/runtime is compatible with existing Silvery components. Key differences:
| Ink | silvery/runtime |
|---|---|
useInput(input, key) | useInput(input, key) (same signature!) |
useApp().exit() | return 'exit' from handler or useExit() |
| Props for callbacks | Store actions (createApp) |
What's Next
When your app outgrows useState and useInput, the progression guide shows how to grow without rewriting:
- Building an App — from Counter to full TEA, one level at a time
For API details, see State Management and Event Handling. You can also explore the Components and Hooks references.