Quickstart¶
Build your first Rezi terminal application in minutes.
Create a New Project¶
Option 1: Scaffold with create-rezi¶
Select a template when prompted (dashboard, form-app, file-browser, or streaming-viewer), or pass --template to choose directly:
Option 2: Manual setup¶
mkdir my-tui-app && cd my-tui-app
npm init -y
npm install @rezi-ui/core @rezi-ui/node typescript tsx
Minimal Example¶
Create index.ts:
import { createApp, ui, rgb } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
type State = { count: number };
const app = createApp<State>({
backend: createNodeBackend(),
initialState: { count: 0 },
});
app.view((state) =>
ui.column({ p: 1, gap: 1 }, [
ui.text("Rezi Counter", { style: { fg: rgb(120, 200, 255), bold: true } }),
ui.box({ title: "Controls", p: 1 }, [
ui.row({ gap: 2 }, [
ui.text(`Count: ${state.count}`),
ui.button({
id: "inc",
label: "+1",
onPress: () => app.update((s) => ({ count: s.count + 1 })),
}),
ui.button({
id: "dec",
label: "-1",
onPress: () => app.update((s) => ({ count: s.count - 1 })),
}),
]),
]),
])
);
// Press 'q' to quit
app.keys({
"q": () => app.stop(),
"ctrl+c": () => app.stop(),
});
await app.start();
Run with:
You should see a counter UI. Use Tab to navigate between buttons, Enter to activate them, and 'q' to quit.
Understanding the Code¶
Creating the Application¶
createApp<State>creates a typed application instancebackendspecifies the rendering backend (Node.js in this case)initialStateprovides the initial application state
Defining the View¶
app.view()registers a function that returns the UI tree- The function receives the current state and returns a
VNode - The view is re-rendered whenever state changes
Widgets and Layout¶
ui.column({ p: 1, gap: 1 }, [
ui.text("Title"),
ui.row({ gap: 2 }, [
ui.button({ id: "btn", label: "Click" }),
]),
])
ui.column()arranges children verticallyui.row()arranges children horizontallyp: 1adds 1 cell of paddinggap: 1adds 1 cell between children
State Updates¶
app.update()updates the state and triggers a re-render- Pass a function that receives the previous state and returns the new state
- Updates are batched and coalesced for efficiency
Keybindings¶
app.keys()registers global keybindings- Keys can include modifiers:
ctrl,alt,shift,meta - Chord sequences are supported:
"g g"(press g twice)
A More Complete Example¶
Here's a todo list application demonstrating more features:
import { createApp, ui, rgb } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
type Todo = { id: string; text: string; done: boolean };
type State = {
todos: Todo[];
selected: number;
input: string;
};
const app = createApp<State>({
backend: createNodeBackend(),
initialState: {
todos: [
{ id: "1", text: "Learn Rezi", done: false },
{ id: "2", text: "Build an app", done: false },
],
selected: 0,
input: "",
},
});
app.view((state) => {
const { todos, selected, input } = state;
return ui.column({ p: 1, gap: 1 }, [
// Title
ui.text("Todo List", { style: { fg: rgb(100, 200, 255), bold: true } }),
// Todo items
ui.box({ title: `Items (${todos.length})`, p: 1 }, [
todos.length === 0
? ui.text("No todos yet", { style: { fg: rgb(128, 128, 128) } })
: ui.column(
{ gap: 0 },
todos.map((todo, i) => {
const isSel = i === selected;
const prefix = isSel ? "> " : " ";
const check = todo.done ? "[x]" : "[ ]";
return ui.text(`${prefix}${check} ${todo.text}`, {
key: todo.id,
style: {
bold: isSel,
dim: todo.done,
fg: todo.done ? rgb(128, 128, 128) : undefined,
},
});
})
),
]),
// Add new todo
ui.row({ gap: 1 }, [
ui.input({
id: "new-todo",
value: input,
onInput: (v) => app.update((s) => ({ ...s, input: v })),
}),
ui.button({
id: "add",
label: "Add",
onPress: () => {
if (input.trim()) {
app.update((s) => ({
...s,
todos: [...s.todos, { id: Date.now().toString(), text: input.trim(), done: false }],
input: "",
}));
}
},
}),
]),
// Help text
ui.text("j/k: navigate | space: toggle | d: delete | q: quit", {
style: { fg: rgb(100, 100, 100) },
}),
]);
});
app.keys({
j: (ctx) =>
ctx.update((s) => ({
...s,
selected: Math.min(s.selected + 1, s.todos.length - 1),
})),
k: (ctx) =>
ctx.update((s) => ({
...s,
selected: Math.max(s.selected - 1, 0),
})),
space: (ctx) =>
ctx.update((s) => ({
...s,
todos: s.todos.map((t, i) =>
i === s.selected ? { ...t, done: !t.done } : t
),
})),
d: (ctx) =>
ctx.update((s) => ({
...s,
todos: s.todos.filter((_, i) => i !== s.selected),
selected: Math.max(0, Math.min(s.selected, s.todos.length - 2)),
})),
q: () => app.stop(),
});
await app.start();
Next Steps¶
- Using JSX - Prefer JSX syntax? Use
@rezi-ui/jsxfor a JSX-based widget API - Concepts - Understand Rezi's architecture
- Widget Catalog - Browse all available widgets
- Layout Guide - Learn about spacing and alignment
- Styling Guide - Customize colors and themes
- Migrating from Ink - Already using Ink? Migrate with one import change
- Examples - More example applications