Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

First Native App

Perry compiles declarative TypeScript UI code to native platform widgets. No Electron, no WebView — real AppKit on macOS, UIKit on iOS, GTK4 on Linux, Win32 on Windows. Every example on this page is a real source file under docs/examples/ that CI compiles and runs on every PR.

A Simple Counter

// demonstrates: minimal stateful UI — label + increment button
// docs: docs/src/ui/state.md
// platforms: macos, linux, windows
// targets: ios-simulator, visionos-simulator, tvos-simulator, watchos-simulator, android, web, wasm

import { App, VStack, Text, Button, State } from "perry/ui"

const count = State(0)

App({
    title: "Counter",
    width: 400,
    height: 300,
    body: VStack(16, [
        Text(`Count: ${count.value}`),
        Button("Increment", () => count.set(count.value + 1)),
    ]),
})

Compile and run:

perry counter.ts -o counter
./counter

A native window opens with a label and two buttons. Clicking “Increment” updates the count in real-time.

How It Works

  • App({ title, width, height, body }) — Creates a native application window. body is the root widget.
  • State(initialValue) — Creates reactive state. .value reads, .set(v) writes and triggers UI updates.
  • VStack(spacing, [...]) — Vertical stack layout (like SwiftUI’s VStack or CSS flexbox column). The first arg is the gap in points between children.
  • Text(string) — A text label. Template literals referencing ${state.value} bind reactively.
  • Button(label, onClick) — A native button with a click handler.

A Todo App

// demonstrates: complete reactive todo app combining State, ForEach, and widget tree mutation
// docs: docs/src/ui/state.md
// platforms: macos, linux, windows
// targets: ios-simulator, tvos-simulator, watchos-simulator, web, wasm

import {
    App,
    Text,
    Button,
    TextField,
    VStack,
    HStack,
    State,
    ForEach,
    Spacer,
    Divider,
} from "perry/ui"

const todos = State<string[]>([])
const count = State(0)
const input = State("")

App({
    title: "Todo App",
    width: 480,
    height: 600,
    body: VStack(16, [
        Text("My Todos"),

        HStack(8, [
            TextField("What needs to be done?", (value: string) => input.set(value)),
            Button("Add", () => {
                const text = input.value
                if (text.length > 0) {
                    todos.set([...todos.value, text])
                    count.set(count.value + 1)
                    input.set("")
                }
            }),
        ]),

        Divider(),

        ForEach(count, (i: number) =>
            HStack(8, [
                Text(todos.value[i]),
                Spacer(),
                Button("Delete", () => {
                    todos.set(todos.value.filter((_, idx) => idx !== i))
                    count.set(count.value - 1)
                }),
            ]),
        ),

        Spacer(),
        Text(`${count.value} items`),
    ]),
})

ForEach(count, render) iterates by index — keep an item array and a count state in sync, then read items via array.value[i] inside the closure. See State Management for the full pattern.

Cross-Platform

The same code runs on all 6 platforms:

# macOS (default)
perry app.ts -o app
./app

# iOS Simulator
perry app.ts -o app --target ios-simulator

# Web (compiles to WebAssembly + DOM bridge in a self-contained HTML file)
perry app.ts -o app --target web   # alias: --target wasm
open app.html

# Other platforms
perry app.ts -o app --target windows
perry app.ts -o app --target linux
perry app.ts -o app --target android

Each target compiles to the platform’s native widget toolkit. See Platforms for details.

Adding Styling

Styling is applied via free functions that take the widget handle as their first argument. Colors are RGBA floats in [0.0, 1.0] — divide a hex byte by 255 to convert (0x33 / 255 ≈ 0.2).

// demonstrates: counter from getting-started/first-app.md with styled widgets
// docs: docs/src/getting-started/first-app.md
// platforms: macos, linux, windows
// targets: ios-simulator, tvos-simulator, watchos-simulator, web, wasm
// run: false

import {
    App, VStack, Text, Button, State,
    textSetFontSize, textSetColor,
    setCornerRadius, setPadding,
    widgetSetBackgroundColor,
} from "perry/ui"

const count = State(0)

const label = Text(`Count: ${count.value}`)
textSetFontSize(label, 24)
textSetColor(label, 0.2, 0.2, 0.2, 1.0)        // RGBA in [0,1] — same as #333333

const btn = Button("Increment", () => count.set(count.value + 1))
setCornerRadius(btn, 8)
widgetSetBackgroundColor(btn, 0.0, 0.478, 1.0, 1.0)  // system blue

const stack = VStack(20, [label, btn])
setPadding(stack, 20, 20, 20, 20)

App({
    title: "Styled Counter",
    width: 400,
    height: 300,
    body: stack,
})

See Styling for all available style properties.

Next Steps