Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 30 additions & 2 deletions docs/backend/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,39 @@ The Node backend owns:
- frame scheduling and buffer pooling
- transfer of drawlists to the engine and event batches back to core

Most apps should construct a backend via:
Most apps should construct the app via:

```ts
import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({
initialState: { count: 0 },
config: {
fpsCap: 60,
maxEventBytes: 1 << 20,
useV2Cursor: false,
},
});
```

`createNodeApp` is the recommended path because it keeps core/backend config
knobs aligned:

- `useV2Cursor` and drawlist v2 are paired automatically.
- `maxEventBytes` is applied to both app parsing and backend worker buffers.
- `fpsCap` is the single scheduling knob.

Advanced path:

```ts
import { createApp } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";

const backend = createNodeBackend({ fpsCap: 60 });
const app = createApp({ backend, initialState: { count: 0 } });
```

Next: [Worker model](worker-model.md).
If advanced config values conflict, Rezi now throws deterministic
`ZRUI_INVALID_PROPS` errors with exact fixes.

Next: [Worker model](worker-model.md).
16 changes: 8 additions & 8 deletions docs/getting-started/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,14 @@ bun add -d typescript tsx
Create `index.ts`:

```typescript
import { createApp, ui, rgb } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
import { ui, rgb } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";

type State = { count: number };

const app = createApp<State>({
backend: createNodeBackend(),
const app = createNodeApp<State>({
initialState: { count: 0 },
config: { fpsCap: 60, maxEventBytes: 1 << 20, useV2Cursor: false },
});

app.view((state) =>
Expand Down Expand Up @@ -107,14 +107,14 @@ You should see a counter UI. Use Tab to navigate between buttons, Enter to activ
### Creating the Application

```typescript
const app = createApp<State>({
backend: createNodeBackend(),
const app = createNodeApp<State>({
initialState: { count: 0 },
config: { fpsCap: 60, maxEventBytes: 1 << 20, useV2Cursor: false },
});
```

- `createApp<State>` creates a typed application instance
- `backend` specifies the rendering backend (`@rezi-ui/node` for Node.js and Bun)
- `createNodeApp<State>` creates a typed application instance and compatible Node backend
- `config` controls app/backend runtime knobs in one place (`fpsCap`, `maxEventBytes`, `useV2Cursor`)
- `initialState` provides the initial application state

### Defining the View
Expand Down
16 changes: 9 additions & 7 deletions docs/guide/lifecycle-and-updates.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,28 @@

This guide explains how a Rezi app moves through its lifecycle, how updates are committed, and what “deterministic scheduling” means in practice.

## `createApp`
## `createNodeApp` (recommended)

Apps are created via `createApp` from `@rezi-ui/core` and a backend from `@rezi-ui/node`:
Apps are usually created through `createNodeApp` from `@rezi-ui/node`:

```typescript
import { createApp, ui } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
import { ui } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";

type State = { count: number };

const app = createApp<State>({
backend: createNodeBackend(),
const app = createNodeApp<State>({
initialState: { count: 0 },
config: { fpsCap: 60 },
config: { fpsCap: 60, maxEventBytes: 1 << 20, useV2Cursor: false },
});

app.view((state) => ui.text(`Count: ${state.count}`));
await app.start();
```

`createNodeApp` keeps app/backend cursor protocol, event caps, and fps knobs in
sync by construction.

## State machine diagram

```mermaid
Expand Down
8 changes: 4 additions & 4 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
Rezi is a **code-first terminal UI framework** for Node.js and Bun. Build rich, interactive terminal applications with a declarative widget API, automatic focus management, and native rendering performance.

```typescript
import { createApp, ui, rgb } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";
import { ui, rgb } from "@rezi-ui/core";
import { createNodeApp } from "@rezi-ui/node";

const app = createApp({
backend: createNodeBackend(),
const app = createNodeApp({
initialState: { count: 0 },
config: { fpsCap: 60, maxEventBytes: 1 << 20, useV2Cursor: false },
});

app.view((state) =>
Expand Down
37 changes: 29 additions & 8 deletions docs/packages/node.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,42 @@ bun add @rezi-ui/node
- A stable message protocol between main thread and worker
- Integration with `@rezi-ui/native` (prebuilt binaries when available)

## Creating a backend
## Creating an app (recommended)

```ts
import { createNodeBackend } from "@rezi-ui/node";

const backend = createNodeBackend({
fpsCap: 60,
import { createNodeApp } from "@rezi-ui/node";

const app = createNodeApp({
initialState: { count: 0 },
config: {
fpsCap: 60,
maxEventBytes: 1 << 20,
useV2Cursor: false,
},
});
```

Pass the backend into `createApp` from `@rezi-ui/core`. This backend is supported in Node.js and Bun runtimes.
`createNodeApp` is the default path because it keeps core/backend config in
lockstep:

- `useV2Cursor` <-> drawlist v2
- app/backend `maxEventBytes`
- app/backend `fpsCap`

## Creating a backend directly (advanced)

```ts
import { createApp } from "@rezi-ui/core";
import { createNodeBackend } from "@rezi-ui/node";

const backend = createNodeBackend({ fpsCap: 60 });
const app = createApp({ backend, initialState: { count: 0 } });
```

## Native engine config passthrough

`createNodeBackend` accepts `nativeConfig`, a JSON-ish object forwarded to the native layer’s engine creation config.
`createNodeBackend` accepts `nativeConfig`, a JSON-ish object forwarded to the
native layer’s engine creation config.

Keys are forwarded as-is. If you want a close match to the engine’s public C structs, use `snake_case` field names:

Expand All @@ -44,7 +65,7 @@ import { createNodeBackend } from "@rezi-ui/node";

const backend = createNodeBackend({
nativeConfig: {
target_fps: 60,
target_fps: 60, // must match fpsCap when provided
limits: {
dl_max_total_bytes: 16 << 20,
},
Expand Down
126 changes: 126 additions & 0 deletions docs/terminal-io-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Terminal I/O Contract

This document defines Rezi's terminal input contract.

Contract path:

1. Raw bytes arrive from a real PTY/ConPTY session.
2. Zireael normalizes bytes into ZREV batches.
3. Rezi parses those batches with `parseEventBatchV1`.
4. App/runtime routing consumes parsed events.

All behavior in this document is covered by deterministic integration tests in:

- `packages/node/src/__e2e__/terminal_io_contract.e2e.test.ts`
- `packages/node/src/__e2e__/fixtures/terminal-io-contract-target.ts`

## Constants

- Key codes follow `@rezi-ui/core/keybindings` / `include/zr/zr_event.h`.
- Modifier bitmask:
- `SHIFT=1`
- `CTRL=2`
- `ALT=4`
- `META=8`

## Keyboard Contract

### Byte-to-event mapping

| Input bytes | Expected parsed event(s) |
| --- | --- |
| `ESC [ 1 ; 5 A` | `{ kind: "key", key: ZR_KEY_UP, mods: ZR_MOD_CTRL, action: "down" }` |
| `ESC [ Z` | `{ kind: "key", key: ZR_KEY_TAB, mods: ZR_MOD_SHIFT, action: "down" }` |
| `ESC [ 9 ; 5 u` | `{ kind: "key", key: ZR_KEY_TAB, mods: ZR_MOD_CTRL, action: "down" }` |
| `ESC [ 13 ; 5 u` | `{ kind: "key", key: ZR_KEY_ENTER, mods: ZR_MOD_CTRL, action: "down" }` |
| `ESC [ 127 ; 5 u` | `{ kind: "key", key: ZR_KEY_BACKSPACE, mods: ZR_MOD_CTRL, action: "down" }` |
| `ESC [ 97 ; 3 u` | Alt/Meta text policy fallback: `ESC` key prefix then payload (`'a'` text or equivalent Alt key payload event) |
| `ESC [ 98 ; 9 u` | Alt/Meta text policy fallback: `ESC` key prefix then payload (`'b'` text or equivalent Meta key payload event) |

### ESC ambiguity/incomplete policy

- Incomplete supported escape prefixes are buffered.
- If a sequence is completed in a later read, it resolves as the completed key event.
- If a supported prefix remains incomplete at flush, fallback is deterministic:
- emit `ESC` key
- emit remaining bytes as text scalars (example: `ESC [` -> `ESC` key then `'['` text)

## Bracketed Paste Contract

### Framing

- Begin marker: `ESC [ 200 ~`
- End marker: `ESC [ 201 ~`
- Payload between markers produces one `paste` event:
- `{ kind:"paste", bytes:<exact payload bytes> }`

### Missing end marker

- Missing end marker must not wedge input.
- Engine may finalize and emit a best-effort `paste` event with captured bytes, or drop the incomplete paste.
- Subsequent key/text input must continue normally.

### Max paste size behavior

- Paste capture is bounded by engine paste buffer capacity.
- On overrun, the oversized paste is dropped (no truncated `paste` event is emitted).
- Input stream continues after paste end or idle flush.

## Focus Contract

### Focus in/out

| Input bytes | Expected parsed event |
| --- | --- |
| `ESC [ I` | `{ kind:"key", key:30 /*FOCUS_IN*/, mods:0, action:"down" }` |
| `ESC [ O` | `{ kind:"key", key:31 /*FOCUS_OUT*/, mods:0, action:"down" }` |

### Gating

- Focus events are emitted when terminal capabilities report focus support.
- Rezi integration coverage asserts capability-gated suppression (`ZIREAEL_CAP_FOCUS_EVENTS=0`).
- Native runtime config gating (`enableFocusEvents`) is implemented by Zireael platform config.

## Mouse Contract (SGR)

| Input bytes | Expected parsed event |
| --- | --- |
| `ESC [ < 0 ; 300 ; 400 M` | `{ kind:"mouse", mouseKind:3 /*down*/, x:299, y:399, buttons:1 }` |
| `ESC [ < 0 ; 300 ; 400 m` | `{ kind:"mouse", mouseKind:4 /*up*/, x:299, y:399, buttons:1 }` |
| `ESC [ < 64 ; 400 ; 500 M` | `{ kind:"mouse", mouseKind:5 /*wheel*/, x:399, y:499, wheelY:1 }` |

Notes:

- SGR coordinates are 1-based on wire and normalized to 0-based in events.
- High coordinates must remain stable (no 223-column legacy clipping).

## Resize Contract

- Engine emits an initial resize event at startup.
- Subsequent terminal size changes emit `resize` events with latest cols/rows.
- Ordering expectation:
- initial resize appears before later explicit resize updates
- observed size values match PTY resize requests

## Split Reads / Partial Sequences

Examples (explicitly tested):

1. Split complete sequence across reads:
- read #1: `ESC [`
- read #2: `A`
- expected output: one `UP` key event, no premature `ESC` fallback before completion.
2. Split incomplete sequence flushed without completion:
- read #1: `ESC [`
- no completion bytes
- expected output after flush: `ESC` key event, then `'['` text event.
3. Split paste begin/content without end marker:
- read #1: `ESC [ 200 ~ xyz`
- no end marker
- expected output: input remains live (and may include a best-effort paste flush).

## Platform Coverage

- Linux/macOS: full contract suite runs through real PTY.
- Windows: ConPTY-guarded test covers at least arrows/modifiers + bracketed paste.
- If ConPTY path is unavailable in environment, tests skip with explicit reason.
32 changes: 32 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading