Skip to content
Closed
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
179 changes: 69 additions & 110 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,143 +1,102 @@
# Gridland

Gridland renders [opentui](https://github.com/nicosalm/opentui) React apps directly in the browser using HTML5 `<canvas>`, bypassing any terminal emulator. Gridland is built on the opentui engine.
Gridland renders [opentui](https://github.com/nicosalm/opentui) React apps directly in the browser using HTML5 `<canvas>`, bypassing any terminal emulator.

![Screenshot](screenshot.png)
## Quick start

## Architecture

```
React JSX -> @opentui/react reconciler -> Renderable tree -> Yoga layout
-> renderSelf() calls buffer.drawText / drawBox / setCell
-> BrowserBuffer (pure JS TypedArrays) stores cell grid
-> CanvasPainter reads buffer -> ctx.fillRect + ctx.fillText
```bash
# Create a new project
npx create-gridland my-app
cd my-app
bun install
bun run dev
```

The key insight: OpenTUI renderables never call Zig directly. They call `OptimizedBuffer` methods. By replacing the buffer + renderer with pure-JS implementations, the entire renderable/reconciler layer works unchanged.
You can choose between **Vite** and **Next.js** during setup.

## What's implemented
## Add to an existing project

- **BrowserBuffer** — Pure-JS replacement for `OptimizedBuffer` using `Uint32Array`/`Float32Array` typed arrays. Same API: `setCell`, `drawText`, `drawBox`, `fillRect`, scissor rect stack, opacity stack.
- **BrowserTextBuffer / BrowserTextBufferView** — Pure-JS replacements for the Zig-backed text storage and word/char wrapping.
- **CanvasPainter** — Two-pass canvas renderer: background rects, then foreground chars with font style attributes (bold/italic/underline).
- **BrowserRenderer** — Orchestrator running `requestAnimationFrame` loop: lifecycle passes -> Yoga layout -> render commands -> paint.
- **BrowserRenderContext** — Implements the `RenderContext` interface (event emitter, lifecycle pass registry, focus management).
- **Vite plugin** — Custom `resolveId` plugin that intercepts all Zig/FFI/Node.js imports from the opentui source tree and redirects them to browser shims.
- **React integration** — `createBrowserRoot()` wires the @opentui/react reconciler to the browser renderer.
```bash
bun add @gridland/web @gridland/utils
```

## Getting started
Then wrap your app with the `<TUI>` component:

```bash
git clone https://github.com/<org>/gridland.git
cd gridland
bun setup
```tsx
import { TUI } from "@gridland/web"
import { useKeyboard } from "@gridland/utils"

function App() {
return (
<TUI style={{ width: "100vw", height: "100vh" }} backgroundColor="#1a1a2e">
{/* your components */}
</TUI>
)
}
```

This installs all dependencies and builds the packages.
**Vite** — add the plugin to your `vite.config.ts`:

## Running
```ts
import { gridlandWebPlugin } from "@gridland/web/vite-plugin"

```bash
# Dev server
bun run dev
# -> http://localhost:5173
export default defineConfig({
plugins: [react(), gridlandWebPlugin()],
})
```

# Tests
bun run test
**Next.js** — use the `@gridland/web/next` export and the webpack plugin:

# Production build
bun run build
```ts
// next.config.ts
import { withGridland } from "@gridland/web/next-plugin"
export default withGridland({})
```

### AI chat demo (Cloudflare Worker)
```tsx
// component.tsx
"use client"
import { TUI } from "@gridland/web/next"
```

The docs site AI chat demo connects to a Cloudflare Worker that proxies requests to [OpenRouter](https://openrouter.ai/). The API key never lives in the repo — it's stored as a Cloudflare Workers secret.
## UI components

**Local development:**
Gridland includes a component registry (shadcn-style). Install individual components into your project:

```bash
# 1. Create a local secrets file (gitignored)
echo "OPENROUTER_API_KEY=sk-or-..." > packages/chat-worker/.dev.vars
bunx shadcn@latest add @gridland/spinner
```

# 2. Start the Worker (localhost:8787)
bun run chat:dev
Available components include Chat, Table, TextInput, SelectInput, Modal, Spinner, StatusBar, and more.

# 3. In another terminal, start the docs site
bun run dev
```
## Packages

The `.env` at the repo root sets `NEXT_PUBLIC_CHAT_API_URL=http://localhost:8787/chat` so the docs site points at the local Worker.
| Package | Description |
|---------|-------------|
| `@gridland/web` | Core browser runtime — `<TUI>` component, canvas renderer, Vite/Next.js plugins |
| `@gridland/utils` | Shared hooks and types (`useKeyboard`, `useOnResize`, color utilities) |
| `@gridland/ui` | Component registry (shadcn-style, not installed directly) |
| `@gridland/testing` | Test utilities — `renderTui()`, `Screen` queries, `KeySender` |
| `@gridland/demo` | Demo framework and landing page |
| `@gridland/bun` | Native Bun runtime with FFI for terminal rendering |
| `create-gridland` | Project scaffolding CLI |

**Production deployment:**
## Development (contributors)

```bash
# Set the secret in Cloudflare (one-time, encrypted at rest)
cd packages/chat-worker && npx wrangler secret put OPENROUTER_API_KEY
git clone git@github.com:thoughtfulllc/gridland.git
cd gridland
bun setup

# Deploy the Worker
bun run chat:deploy
```
# Run the docs site
bun run dev

Then set `NEXT_PUBLIC_CHAT_API_URL` to the deployed Worker URL in your static hosting environment (e.g. Render dashboard). This env var is baked into the static bundle at build time.
# Run tests
bun run test

## Project structure
# Run e2e tests
bun run test:e2e

# Build all packages
bun run build
```
packages/
web/ # Core browser runtime (npm: @gridland/web)
src/
index.ts # Main exports (bundled mode)
core.ts # Core exports (external mode for Vite plugin users)
TUI.tsx # Single React component — THE mounting layer
mount.ts # Imperative mount API: mountGridland(canvas, element)
browser-buffer.ts
browser-text-buffer.ts
browser-text-buffer-view.ts
browser-renderer.ts
browser-render-context.ts
canvas-painter.ts
selection-manager.ts
vite-plugin.ts # Vite plugin for shim resolution
next.ts # Next.js export (thin — just "use client" re-export)
next-plugin.ts # Next.js webpack plugin
utils.ts # SSR-safe utilities
core-shims/ # @opentui/core browser replacements
shims/ # Node.js built-in stubs
__tests__/ # Unit + integration tests

core/ # Hard-forked opentui engine (private)

ui/ # UI component library (npm: @gridland/ui)
components/ # Components with tests

testing/ # Testing utilities (npm: @gridland/testing)
src/

utils/ # Portable hooks & utilities (npm: @gridland/utils)

chat-worker/ # Cloudflare Worker — AI chat proxy
src/index.ts # CORS + streaming via OpenRouter
wrangler.toml # Worker configuration

bun/ # Native Bun runtime for CLI (npm: @gridland/bun)
demo/ # CLI demo runner (npm: @gridland/demo)
create-gridland/ # Project scaffolder CLI
container/ # Docker sandbox runner
docs/ # Fumadocs documentation site (static export)

examples/
vite-example/ # Minimal Vite example
next-example/ # Next.js example
container-demo/ # Docker container demo

e2e/ # Playwright E2E tests
```

## How the Vite plugin works

The opentui source tree (`packages/core/`) is loaded directly. A custom plugin intercepts imports at resolution time:

1. **File-level redirects** — Relative imports that resolve to zig-dependent files (buffer, text-buffer, text-buffer-view, syntax-style, renderer, etc.) are redirected to browser shims.
2. **Pattern-based interception** — tree-sitter, hast, and Node.js builtin imports are caught by string matching and redirected to stubs.
3. **Barrel routing** — `@opentui/core` is routed to the real opentui barrel when imported from the react package (to preserve the original module evaluation order), and to our core-shims barrel when imported from our own code.
4. **Circular dep fix** — `Slider.ts`'s import of `../index` is redirected to a minimal deps file to break a barrel-level circular dependency that causes TDZ errors in strict ESM.
Loading