Termcast is a framework for building terminal user interfaces (TUIs) using React. It implements the Raycast extension API for the terminal, so you can port existing Raycast extensions or build new TUIs with a familiar, battle-tested component model.
bun install -g termcastRequires Bun. Does not work with Node.js.
The simplest way. Create a folder with a package.json and React components. Termcast handles dev server, compilation, and distribution.
my-app/
package.json # name, commands, preferences
src/
index.tsx # default export is a React component
// src/index.tsx
import { List, Action, ActionPanel, showToast, Toast } from 'termcast'
export default function Command() {
return (
<List searchBarPlaceholder="Search items...">
<List.Item
title="Hello World"
actions={
<ActionPanel>
<Action
title="Greet"
onAction={async () => {
await showToast({ style: Toast.Style.Success, title: 'Hi!' })
}}
/>
</ActionPanel>
}
/>
</List>
)
}Run it:
termcast devIf you're porting a Raycast extension, imports from @raycast/api and @raycast/utils work out of the box. Termcast aliases them at build time, so existing code runs without changes. For new code, import from termcast and @termcast/utils instead.
For CLIs that combine regular commands with a TUI mode. Use any CLI framework (like goke) and call renderWithProviders when you need a TUI screen.
This is how zele (a Gmail CLI) works: regular commands for sending, searching, and archiving emails, plus a TUI mode for browsing your inbox.
// src/cli.ts
import { goke } from 'goke'
const cli = goke('myapp')
cli.command('list', 'List items as text').action(async () => {
console.log('item 1\nitem 2\nitem 3')
})
cli.command('', 'Browse items in TUI').action(async () => {
const { renderWithProviders } = await import('termcast')
const { default: BrowseItems } = await import('./browse.js')
const React = await import('react')
await renderWithProviders(React.createElement(BrowseItems), {
extensionName: 'myapp',
})
})
cli.help()
cli.parse()// src/browse.tsx
import { List, Detail, Action, ActionPanel, useNavigation } from 'termcast'
import { useCachedPromise } from '@termcast/utils'
export default function BrowseItems() {
const { data, isLoading } = useCachedPromise(async () => {
const res = await fetch('https://api.example.com/items')
return res.json()
}, [])
return (
<List isLoading={isLoading} searchBarPlaceholder="Search...">
{data?.map((item) => (
<List.Item key={item.id} title={item.name} subtitle={item.status} />
))}
</List>
)
}termcast new my-extension
cd my-extension
termcast devtermcast new scaffolds an extension from a template with a List, Form, actions, and search. termcast dev starts the TUI with hot module reloading: edit your code and the UI updates instantly without restarting.
Run an extension in development mode with HMR. Watches for file changes and rebuilds automatically. This is what you use during development.
termcast dev # current directory
termcast dev ./my-app # specific pathCompile the extension into a standalone executable. The output is a single binary that includes Bun, your code, and all dependencies. No runtime needed on the target machine.
termcast compile
termcast compile -o ./bin/myappCompile for all platforms (macOS arm64/x64, Linux arm64/x64, Windows) and upload the binaries to a GitHub release. After publishing, you get an install script URL:
termcast releaseInstall script:
curl -sf https://termcast.app/owner/repo/install | bash
Share this URL so anyone can install your TUI with one command. See tuitube for a real example:
curl -sf https://termcast.app/r/tuitube | bashBuild a standalone desktop app (macOS .app, Linux, Windows) with a bundled terminal emulator. No terminal needed to run the app.
termcast app build
termcast app build --name "My App" --icon ./icon.png
termcast app build --release # upload to GitHub releaseOptions: --font, --font-size, --theme, --bundle-id, --platform, --arch.
Scaffold a new extension from the built-in template.
termcast new my-extensionThe primary component. Shows a searchable, navigable list of items with optional detail panel, sections, dropdown filters, and pagination.
import { List, Action, ActionPanel, Icon, Color } from 'termcast'
function Repos() {
return (
<List
isShowingDetail={true}
searchBarPlaceholder="Search repos..."
navigationTitle="My Repos"
>
<List.Section title="Active">
<List.Item
title="termcast"
subtitle="TUI framework"
icon={Icon.Star}
accessories={[
{ tag: { value: 'TypeScript', color: Color.Blue } },
{ text: '2 days ago' },
]}
detail={
<List.Item.Detail
markdown="# termcast\n\nBuild terminal apps with React."
metadata={
<List.Item.Detail.Metadata>
<List.Item.Detail.Metadata.Label title="Stars" text="420" />
<List.Item.Detail.Metadata.Separator />
<List.Item.Detail.Metadata.TagList title="Topics">
<List.Item.Detail.Metadata.TagList.Item text="react" color={Color.Blue} />
<List.Item.Detail.Metadata.TagList.Item text="tui" color={Color.Green} />
</List.Item.Detail.Metadata.TagList>
</List.Item.Detail.Metadata>
}
/>
}
actions={
<ActionPanel>
<Action title="Open" onAction={() => {}} />
<Action.CopyToClipboard title="Copy URL" content="https://github.com/..." />
</ActionPanel>
}
/>
</List.Section>
</List>
)
}List features: search bar, sections, dropdown filter (List.Dropdown), detail panel with markdown + metadata, accessories (tags, text, icons), keyboard navigation, infinite scroll pagination.
Full-screen markdown view with optional metadata sidebar. Use for displaying rich content like documentation, email threads, or reports.
import { Detail, Color } from 'termcast'
function ServerStatus() {
return (
<Detail
markdown={`# Server Status\n\nAll systems operational.\n\n| Service | Status |\n|---|---|\n| API | ✓ Running |\n| DB | ✓ Running |`}
metadata={
<Detail.Metadata>
<Detail.Metadata.Label title="Uptime" text="14d 3h" />
<Detail.Metadata.Label
title="CPU"
text={{ value: '42%', color: Color.Orange }}
/>
<Detail.Metadata.Separator />
<Detail.Metadata.TagList title="Regions">
<Detail.Metadata.TagList.Item text="us-east-1" color={Color.Green} />
<Detail.Metadata.TagList.Item text="eu-west-1" color={Color.Blue} />
</Detail.Metadata.TagList>
</Detail.Metadata>
}
/>
)
}Collect user input with text fields, dropdowns, checkboxes, tag pickers, date pickers, and file pickers.
import { Form, Action, ActionPanel, showToast, Toast } from 'termcast'
function CreateIssue() {
return (
<Form
actions={
<ActionPanel>
<Action.SubmitForm
title="Create Issue"
onSubmit={async (values) => {
await showToast({ style: Toast.Style.Success, title: `Created: ${values.title}` })
}}
/>
</ActionPanel>
}
>
<Form.TextField id="title" title="Title" placeholder="Bug report..." />
<Form.TextArea id="description" title="Description" />
<Form.Dropdown id="priority" title="Priority" defaultValue="medium">
<Form.Dropdown.Item value="low" title="Low" />
<Form.Dropdown.Item value="medium" title="Medium" />
<Form.Dropdown.Item value="high" title="High" />
</Form.Dropdown>
<Form.Checkbox id="blocking" title="Blocking" label="Blocks release" />
<Form.DatePicker id="due" title="Due Date" />
</Form>
)
}Form controls: TextField, TextArea, Dropdown, Checkbox, DatePicker, TagPicker, FilePicker, PasswordField, Separator, Description.
Push and pop views onto a stack, like a mobile app.
import { List, Detail, Action, ActionPanel, useNavigation } from 'termcast'
function ItemList() {
const { push } = useNavigation()
return (
<List>
<List.Item
title="View Details"
actions={
<ActionPanel>
<Action.Push title="Open" target={<ItemDetail />} />
<Action
title="Open Programmatically"
onAction={() => { push(<ItemDetail />) }}
/>
</ActionPanel>
}
/>
</List>
)
}
function ItemDetail() {
const { pop } = useNavigation()
return (
<Detail
markdown="# Item Detail"
actions={
<ActionPanel>
<Action title="Go Back" onAction={() => { pop() }} />
</ActionPanel>
}
/>
)
}Actions appear in a panel when you press Ctrl+P. They support keyboard shortcuts, sections, and built-in types like copy, push, open URL, and submit form.
<ActionPanel>
<ActionPanel.Section title="Primary">
<Action title="Open" onAction={() => {}} />
<Action title="Edit" shortcut={{ modifiers: ['ctrl'], key: 'e' }} onAction={() => {}} />
</ActionPanel.Section>
<ActionPanel.Section title="Clipboard">
<Action.CopyToClipboard title="Copy ID" content="abc-123" />
<Action.Paste title="Paste" content="hello" />
</ActionPanel.Section>
<ActionPanel.Section>
<Action.Push title="View Details" target={<Detail markdown="..." />} />
<Action.OpenInBrowser title="GitHub" url="https://github.com" />
<Action.SubmitForm title="Save" onSubmit={handleSubmit} />
</ActionPanel.Section>
</ActionPanel>Non-blocking notifications. Supports animated (loading), success, and failure styles with optional actions.
import { showToast, Toast } from 'termcast'
// Simple
await showToast({ style: Toast.Style.Success, title: 'Saved' })
// With loading state
const toast = await showToast({ style: Toast.Style.Animated, title: 'Downloading...', message: '0%' })
// ... update progress
toast.message = '50%'
// ... done
toast.style = Toast.Style.Success
toast.title = 'Downloaded'The @termcast/utils package provides hooks for async data loading, caching, and form management.
Fetches data with automatic caching, revalidation, and loading state. Supports pagination for infinite scroll lists.
import { List } from 'termcast'
import { useCachedPromise } from '@termcast/utils'
function EmailList() {
const { data, isLoading, revalidate, pagination } = useCachedPromise(
(query: string) => async ({ cursor }) => {
const res = await fetchEmails({ query, pageToken: cursor })
return {
data: res.emails,
hasMore: !!res.nextPageToken,
cursor: res.nextPageToken,
}
},
[''],
{ keepPreviousData: true },
)
return (
<List isLoading={isLoading} pagination={pagination}>
{data?.map((email) => (
<List.Item key={email.id} title={email.subject} subtitle={email.from} />
))}
</List>
)
}Persistent state that survives across restarts. Backed by SQLite.
import { useCachedState } from '@termcast/utils'
const [selectedFolder, setSelectedFolder] = useCachedState('activeFolder', 'inbox', {
cacheNamespace: 'mail',
})Like useCachedPromise but without caching. Good for one-shot fetches.
import { usePromise } from '@termcast/utils'
const { data: video, isLoading } = usePromise(
async (url: string) => {
const result = await fetchVideoMetadata(url)
return result
},
[videoUrl],
{
onError(error) {
showToast({ style: Toast.Style.Failure, title: 'Not found', message: error.message })
},
},
)Form validation and submission handling.
import { useForm } from '@termcast/utils'
const { handleSubmit, itemProps } = useForm({
onSubmit: async (values) => {
await saveItem(values)
},
validation: {
url: (value) => {
if (!value) return 'URL is required'
if (!isValidUrl(value)) return 'Invalid URL'
},
},
})import { showFailureToast } from '@termcast/utils'
try {
await riskyOperation()
} catch (error) {
await showFailureToast(error, { title: 'Operation failed' })
}These components go beyond the Raycast API. They render natively in the terminal using braille characters, block elements, and box-drawing characters.
Renders line charts using braille characters. Supports multiple series, Y-axis labels, and area fill.
import { Detail, Graph, Color } from 'termcast'
<Detail
markdown="# Stock Price"
metadata={
<Graph height={15} xLabels={['Jan', 'Apr', 'Jul', 'Oct']} yTicks={6} yFormat={(v) => `$${v.toFixed(0)}`}>
<Graph.Line data={[150, 162, 175, 190, 185, 201]} color={Color.Orange} title="AAPL" />
</Graph>
}
/>Variants: "line" (default), "area" (filled area below the line).
Proportional horizontal bar with labeled segments. Good for budgets, disk usage, portfolios.
import { BarChart, Color } from 'termcast'
<BarChart height={1}>
<BarChart.Segment value={4850} label="Spent" />
<BarChart.Segment value={707} label="Remaining" />
<BarChart.Segment value={617} label="Savings" color={Color.Green} />
</BarChart>Vertical bar chart with █ fill, gaps between bars, X-axis labels, and a compact legend.
import { BarGraph } from 'termcast'
<BarGraph height={15} labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}>
<BarGraph.Series data={[40, 30, 25, 15, 50]} title="Direct" />
<BarGraph.Series data={[30, 35, 15, 20, 35]} title="Organic" />
</BarGraph>Like BarGraph but horizontal. Each row is a stacked bar with a label column and right-side legend.
import { HorizontalBarGraph } from 'termcast'
<HorizontalBarGraph height={10} labels={['Mon', 'Tue', 'Wed', 'Thu', 'Fri']}>
<HorizontalBarGraph.Series data={[40, 30, 25, 15, 50]} title="Views" />
<HorizontalBarGraph.Series data={[20, 25, 10, 10, 25]} title="Clicks" />
</HorizontalBarGraph>Candlestick charts for financial data. Green bars are bullish (close >= open), red bars are bearish.
import { CandleChart } from 'termcast'
<CandleChart
data={candles} // Array<{ open, close, high, low }>
height={12}
xLabels={['12d', '8d', '4d', 'Now']}
yTicks={4}
yFormat={(v) => `$${v.toFixed(2)}`}
/>GitHub-style contribution heatmap. Shows daily activity over months with configurable colors.
import { CalendarHeatmap, Color } from 'termcast'
<CalendarHeatmap
data={dailyData} // Array<{ date: Date, value: number }>
color={Color.Green}
/>Rich tables with markdown formatting in cells (bold, italic, links, code).
import { Table } from 'termcast'
<Table
headers={['Region', 'Latency', 'Status']}
rows={[
['us-east-1', '**12ms**', '`ok`'],
['eu-west-1', '*45ms*', '`ok`'],
['ap-south-1', '`89ms`', '`warn`'],
]}
/>Usage-style progress bar with percentage and label.
import { ProgressBar } from 'termcast'
<ProgressBar title="Current session" value={37} percentageSuffix="used" label="Resets 9pm" />Place components side by side. Useful for comparing charts, tables, or any metadata.
import { Row, BarGraph } from 'termcast'
<Row>
<BarGraph height={10} labels={['Mon', 'Tue', 'Wed']}>
<BarGraph.Series data={[40, 30, 25]} title="Week 1" />
</BarGraph>
<BarGraph height={10} labels={['Mon', 'Tue', 'Wed']}>
<BarGraph.Series data={[50, 40, 35]} title="Week 2" />
</BarGraph>
</Row>Inline markdown rendering inside metadata panels.
import { Markdown } from 'termcast'
<Detail.Metadata>
<Markdown content="**Status:** All systems operational. See [docs](https://example.com) for details." />
</Detail.Metadata>Use termcast as a library without the CLI or extension system. Call renderWithProviders to mount any React component with all termcast infrastructure (navigation, storage, query cache, theme):
import { renderWithProviders, List } from 'termcast'
function MyApp() {
return <List><List.Item title="Hello" /></List>
}
await renderWithProviders(<MyApp />, {
extensionName: 'my-app',
})| Option | Default | Description |
|---|---|---|
extensionName |
'termcast-app' |
Derives storage paths and extension metadata |
extensionPath |
~/.termcast/compiled/{extensionName} |
Where LocalStorage and Cache are stored |
packageJson |
{ name, title, description: '', commands: [] } |
Extension metadata for preferences |
- Uses Bun instead of Node
- Renders in a terminal instead of a macOS app
- Cross-platform: macOS, Linux, Windows
- Compiles to standalone executables
- Can be bundled as a desktop app (
.appon macOS) - Superset of the Raycast API with terminal-native components (charts, tables, heatmaps)
- Best-effort API compatibility, not a drop-in replacement