1ch
v0.3.0
Published
Terminal UI components for React
Downloads
194
Readme
1ch
Terminal UI components for React. For those who'd rather be in the terminal.

The smallest unit is one character cell. A button is [ OK ]. A progress bar is ████░░░░. A table border is │ and ─ and ┼. The whole thing is a grid.
npm install 1ch react react-domWhat it looks like
import { TermUI, TBox, TVStack, TTable, TBar } from "1ch";
import "1ch/style.css";
function Dashboard() {
return (
<TermUI width={60}>
<TBox title="Server Status">
<TVStack gap={1}>
<TBar label="CPU" value={73} max={100} />
<TBar label="MEM" value={4.2} max={8} />
<TTable
columns={[
{ key: "service", width: 20 },
{ key: "status", width: 10 },
{ key: "uptime", width: 12 },
]}
data={[
{ service: "api-gateway", status: "UP", uptime: "14d 3h" },
{ service: "worker-pool", status: "UP", uptime: "14d 3h" },
{ service: "cache", status: "WARN", uptime: "2h 41m" },
]}
/>
</TVStack>
</TBox>
</TermUI>
);
}Why this exists
Most UI components are styled <div>s pretending to be something else. Terminal UIs skip the pretense - every character is either content or structure, and the grid makes boundaries obvious.
Under the hood, 1ch uses a custom React reconciler that turns your component tree into character grids. React handles lifecycle, hooks, state. The reconciler handles layout. Resize the container and everything reflows.
Components
Layout - TVStack, THStack, TBox, TSeparator, TBlank, TLine, TSpan
Data - TTable, TTree, TList, TCode, TUnifiedDiff, TDiffCompute, TBar, TSpark
Interactive - TTabs, TButton, TStatusbar
Documents - TMarkdown, THtml, TJson
The document components accept raw strings. Feed TMarkdown a markdown string, THtml an HTML string, or TJson a JSON object, and you get terminal-styled output back.
A more complete example
import { TermUI, TBox, TCode, TTabs, TTab, TTable, TBar, TVStack } from "1ch";
import "1ch/style.css";
function App() {
const [tab, setTab] = useState(0);
return (
<TermUI width={80}>
<TTabs active={tab} onSelect={setTab}>
<TTab name="Overview">
<TBox title="Status">
<TVStack>
<TBar label="CPU" value={73} max={100} />
<TBar label="MEM" value={4.2} max={8} />
</TVStack>
</TBox>
</TTab>
<TTab name="Logs">
<TTable
columns={[
{ key: "time", width: 12 },
{ key: "level", width: 8 },
{ key: "message", width: 40 },
]}
data={[
{
time: "14:03:21",
level: "INFO",
message: "Server started on :3000",
},
{
time: "14:03:22",
level: "INFO",
message: "Connected to database",
},
{ time: "14:05:01", level: "WARN", message: "Slow query: 2.3s" },
]}
/>
</TTab>
<TTab name="Config">
<TCode
code={`export default {
port: 3000,
database: "postgres://localhost/app",
cache: { ttl: 300, max: 1000 },
};`}
language="typescript"
title="config.ts"
/>
</TTab>
</TTabs>
</TermUI>
);
}Don't want React? Skip it.
Every component has an imperative equivalent that returns a layout function. Call it with a width and get a character grid back.
import { box, vstack, table, code, bar } from "1ch";
const layout = vstack(
box(
vstack(
bar(73, 100, 40),
table(
[
{ key: "name", header: "Name", width: 20 },
{ key: "value", header: "Value", width: 10 },
],
[
{ name: "requests", value: "1.2k/s" },
{ name: "errors", value: "0.03%" },
]
)
),
{ title: "Metrics" }
),
code(`console.log("hello")`, { language: "javascript" })
);
const block = layout(80);Theming
Dark and light themes built in, and are switchable at runtime. You can override everything via JSON - palette, semantic colors, syntax highlighting, markdown rendering, component tokens. Use parseThemeSpec() to check the JSON before applying it.
import {
TermThemeProvider,
TermUI,
parseThemeSpec,
defaultThemeSpec,
} from "1ch";
const parsed = parseThemeSpec(userThemeJson);
const spec = parsed.ok ? parsed.theme : defaultThemeSpec;
<TermThemeProvider initialTheme={spec} initialMode="system">
<TermUI width={80}>{/* components inherit the theme */}</TermUI>
</TermThemeProvider>;Docs
Full API reference, theme spec details, hooks (useSpinner, useTick, useTermWidth, useStreamingText), and the document pipeline - all in the docs (coming soon).
License
MIT
