Introduction
Perry is a native TypeScript compiler that compiles TypeScript source code directly to native executables. No JavaScript runtime, no JIT warmup, no V8 — your TypeScript compiles to a real binary.
// hello.ts
console.log("Hello from Perry!");
$ perry hello.ts -o hello
$ ./hello
Hello from Perry!
Why Perry?
- Native performance — Compiles to machine code via Cranelift. Integer-heavy code like Fibonacci runs 2x faster than Node.js.
- Small binaries — A hello world is ~300KB. Perry detects what runtime features you use and only links what’s needed.
- Native UI — Build desktop and mobile apps with declarative TypeScript that compiles to real AppKit, UIKit, GTK4, Win32, or DOM widgets.
- 6 platforms — macOS, iOS, Android, Windows, Linux, and Web from the same source code.
- Familiar ecosystem — Use npm packages like
fastify,mysql2,redis,bcrypt,lodash, and more — compiled natively. - Zero config — Point Perry at a
.tsfile and get a binary. Notsconfig.jsonrequired.
What Perry Compiles
Perry supports a practical subset of TypeScript:
- Variables, functions, classes, enums, interfaces
- Async/await, closures, generators
- Destructuring, spread, template literals
- Arrays, Maps, Sets, typed arrays
- Regular expressions, JSON, Promises
- Module imports/exports
- Generic type erasure
See Supported Features for the complete list.
Quick Example: Native App
import { App, Text, Button, VStack, State } from "perry/ui";
const count = State(0);
App("Counter", () =>
VStack([
Text(`Count: ${count.get()}`),
Button("Increment", () => count.set(count.get() + 1)),
])
);
$ perry counter.ts -o counter
$ ./counter # Opens a native macOS/Windows/Linux window
This produces a ~3MB native app with real platform widgets — no Electron, no WebView.
How It Works
TypeScript (.ts)
↓ Parse (SWC)
↓ Lower to HIR
↓ Transform (inline, closure conversion, async)
↓ Codegen (Cranelift)
↓ Link (system linker)
↓
Native Executable
Perry uses SWC for TypeScript parsing and Cranelift for native code generation. Types are erased at compile time (like tsc), and values are represented at runtime using NaN-boxing for efficient 64-bit tagged values.
Next Steps
Installation
Prerequisites
-
Rust toolchain — Perry is built with Cargo. Install via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -
System linker — Perry uses your system’s C compiler to link:
- macOS: Xcode Command Line Tools (
xcode-select --install) - Linux:
gccorclang(apt install build-essential) - Windows: MSVC via Visual Studio Build Tools
- macOS: Xcode Command Line Tools (
Install Perry
From Source (recommended)
git clone https://github.com/skelpo/perry.git
cd perry
cargo build --release
The binary is at target/release/perry. Add it to your PATH:
# Add to ~/.zshrc or ~/.bashrc
export PATH="/path/to/perry/target/release:$PATH"
Self-Update
Once installed, Perry can update itself:
perry update
This downloads the latest release and atomically replaces the binary.
Verify Installation
perry doctor
This checks your installation, shows the current version, and reports if an update is available.
perry --version
Platform-Specific Setup
macOS
No additional setup needed. Perry uses the system cc linker and AppKit for UI apps.
For iOS development, install Xcode (not just Command Line Tools) for the iOS SDK and simulator.
Linux
Install GTK4 development libraries for UI apps:
# Ubuntu/Debian
sudo apt install libgtk-4-dev
# Fedora
sudo dnf install gtk4-devel
Windows
Install Visual Studio Build Tools with the “Desktop development with C++” workload.
What’s Next
Hello World
Your First Program
Create a file called hello.ts:
console.log("Hello, Perry!");
Compile and run it:
perry hello.ts -o hello
./hello
Output:
Hello, Perry!
That’s it. Perry compiled your TypeScript to a native executable — no Node.js, no bundler, no runtime.
A Slightly Bigger Example
function fibonacci(n: number): number {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
const start = Date.now();
const result = fibonacci(40);
const elapsed = Date.now() - start;
console.log(`fibonacci(40) = ${result}`);
console.log(`Completed in ${elapsed}ms`);
perry fib.ts -o fib
./fib
This runs about 2x faster than Node.js because Perry compiles to native machine code with integer specialization.
Using Variables and Functions
const name: string = "World";
const items: number[] = [1, 2, 3, 4, 5];
const doubled = items.map((x) => x * 2);
const sum = doubled.reduce((acc, x) => acc + x, 0);
console.log(`Hello, ${name}!`);
console.log(`Sum of doubled: ${sum}`);
Async Code
async function fetchData(): Promise<string> {
const response = await fetch("https://httpbin.org/get");
const data = await response.json();
return data.origin;
}
const ip = await fetchData();
console.log(`Your IP: ${ip}`);
perry fetch.ts -o fetch
./fetch
Perry compiles async/await to a native async runtime backed by Tokio.
What the Compiler Produces
When you run perry file.ts -o output, Perry:
- Parses your TypeScript with SWC
- Lowers the AST to an intermediate representation (HIR)
- Applies optimizations (inlining, closure conversion, etc.)
- Generates native machine code with Cranelift
- Links with your system’s C compiler
The result is a standalone executable with no external dependencies.
Binary Size
| Program | Binary Size |
|---|---|
| Hello world | ~300KB |
| CLI with fs/path | ~3MB |
| UI app | ~3MB |
| Full app with stdlib | ~48MB |
Perry automatically detects which runtime features you use and only links what’s needed.
Next Steps
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.
A Simple Counter
Create counter.ts:
import { App, Text, Button, VStack, State } from "perry/ui";
const count = State(0);
App("My Counter", () =>
VStack([
Text(`Count: ${count.get()}`),
Button("Increment", () => {
count.set(count.get() + 1);
}),
Button("Reset", () => {
count.set(0);
}),
])
);
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, renderFn)— Creates a native application window. The render function defines the UI.State(initialValue)— Creates reactive state. When you call.set(), the UI re-renders.VStack([...])— Vertical stack layout (like SwiftUI’s VStack or CSS flexbox column).Text(string)— A text label. Template literals with${state.get()}update reactively.Button(label, onClick)— A native button with a click handler.
A Todo App
import { App, Text, Button, TextField, VStack, HStack, State, ForEach } from "perry/ui";
const todos = State<string[]>([]);
const input = State("");
App("Todo App", () =>
VStack([
HStack([
TextField(input, "Add a todo..."),
Button("Add", () => {
const text = input.get();
if (text.length > 0) {
todos.set([...todos.get(), text]);
input.set("");
}
}),
]),
ForEach(todos, (todo, index) =>
HStack([
Text(todo),
Button("Remove", () => {
const items = todos.get();
todos.set(items.filter((_, i) => i !== index));
}),
])
),
])
);
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 (generates HTML)
perry app.ts -o app --target web
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
import { App, Text, Button, VStack, State } from "perry/ui";
const count = State(0);
App("Styled Counter", () => {
const label = Text(`Count: ${count.get()}`);
label.setFontSize(24);
label.setColor("#333333");
const btn = Button("Increment", () => count.set(count.get() + 1));
btn.setCornerRadius(8);
btn.setBackgroundColor("#007AFF");
const stack = VStack([label, btn]);
stack.setPadding(20);
return stack;
});
See Styling for all available style properties.
Next Steps
- Project Configuration — Set up
package.jsonfor Perry projects - UI Overview — Complete guide to Perry’s UI system
- Widgets Reference — All available widgets
- State Management — Reactive state and bindings
Project Configuration
Perry projects use standard package.json for configuration. No special config file is required for basic usage, but larger projects benefit from Perry-specific settings.
Basic Setup
perry init my-project
cd my-project
This creates a package.json and a starter src/index.ts.
package.json
{
"name": "my-project",
"version": "1.0.0",
"main": "src/index.ts",
"perry": {
"compilePackages": []
}
}
Perry Configuration
The perry field in package.json controls compiler behavior:
compilePackages
List npm packages to compile natively instead of routing through the JavaScript runtime:
{
"perry": {
"compilePackages": ["@noble/curves", "@noble/hashes"]
}
}
When a package is listed here, Perry:
- Resolves the package in
node_modules/ - Prefers TypeScript source (
src/index.ts) over compiled JavaScript (lib/index.js) - Compiles all functions natively through Cranelift
- Deduplicates across nested
node_modules/to prevent duplicate linker symbols
This is useful for pure TypeScript/JavaScript packages that don’t rely on Node.js APIs. Packages that use native bindings, eval(), or dynamic require() won’t work.
Using npm Packages
Perry natively supports many popular npm packages without any configuration:
import fastify from "fastify";
import mysql from "mysql2/promise";
import Redis from "ioredis";
import bcrypt from "bcrypt";
These are compiled to native code using Perry’s built-in implementations. See Standard Library for the full list.
For packages not natively supported, use compilePackages for pure TS/JS packages, or the JavaScript runtime fallback for complex packages.
Project Structure
Perry is flexible about project structure. Common patterns:
my-project/
├── package.json
├── src/
│ └── index.ts
└── node_modules/ # Only needed for compilePackages
For UI apps:
my-app/
├── package.json
├── src/
│ ├── index.ts # Main app entry
│ └── components/ # UI components
└── assets/ # Images, etc.
Compilation
# Compile a file
perry src/index.ts -o build/app
# Compile with a specific target
perry src/index.ts -o build/app --target ios-simulator
# Debug: print intermediate representation
perry src/index.ts --print-hir
See CLI Commands for all options.
Next Steps
- CLI Commands — All compiler commands and flags
- Supported Features — What TypeScript features work
- Standard Library — Supported npm packages
Supported TypeScript Features
Perry compiles a practical subset of TypeScript to native code. This page lists what’s supported.
Primitive Types
const n: number = 42;
const s: string = "hello";
const b: boolean = true;
const u: undefined = undefined;
const nl: null = null;
All primitives are represented as 64-bit NaN-boxed values at runtime.
Variables and Constants
let x = 10;
const y = "immutable";
var z = true; // var is supported but let/const preferred
Perry infers types from initializers — let x = 5 is inferred as number without an explicit annotation.
Functions
function add(a: number, b: number): number {
return a + b;
}
// Optional parameters
function greet(name: string, greeting: string = "Hello"): string {
return `${greeting}, ${name}!`;
}
// Rest parameters
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
// Arrow functions
const double = (x: number) => x * 2;
Classes
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
speak(): string {
return `${this.name} makes a noise`;
}
}
class Dog extends Animal {
speak(): string {
return `${this.name} barks`;
}
}
// Static methods
class Counter {
private static instance: Counter;
private count: number = 0;
static getInstance(): Counter {
if (!Counter.instance) {
Counter.instance = new Counter();
}
return Counter.instance;
}
}
Supported class features:
- Constructors
- Instance and static methods
- Instance and static properties
- Inheritance (
extends) - Method overriding
instanceofchecks (via class ID chain)- Singleton patterns (static method return type inference)
Enums
// Numeric enums
enum Direction {
Up,
Down,
Left,
Right,
}
// String enums
enum Color {
Red = "RED",
Green = "GREEN",
Blue = "BLUE",
}
const dir = Direction.Up;
const color = Color.Red;
Enums are compiled to constants and work across modules.
Interfaces and Type Aliases
interface User {
name: string;
age: number;
email?: string;
}
type Point = { x: number; y: number };
type StringOrNumber = string | number;
type Callback = (value: number) => void;
Interfaces and type aliases are erased at compile time (like tsc). They exist only for documentation and editor tooling.
Arrays
const nums: number[] = [1, 2, 3];
// Array methods
nums.push(4);
nums.pop();
const len = nums.length;
const doubled = nums.map((x) => x * 2);
const filtered = nums.filter((x) => x > 2);
const sum = nums.reduce((acc, x) => acc + x, 0);
const found = nums.find((x) => x === 3);
const idx = nums.indexOf(3);
const joined = nums.join(", ");
const sliced = nums.slice(1, 3);
nums.splice(1, 1);
nums.unshift(0);
const sorted = nums.sort((a, b) => a - b);
const reversed = nums.reverse();
const includes = nums.includes(3);
const every = nums.every((x) => x > 0);
const some = nums.some((x) => x > 2);
nums.forEach((x) => console.log(x));
const flat = [[1, 2], [3]].flat();
const concatted = nums.concat([5, 6]);
// Array.from
const arr = Array.from(someIterable);
// Array.isArray
if (Array.isArray(value)) { /* ... */ }
// for...of iteration
for (const item of nums) {
console.log(item);
}
Objects
const obj = { name: "Perry", version: 1 };
obj.name = "Perry 2";
// Dynamic property access
const key = "name";
const val = obj[key];
// Object.keys, Object.values, Object.entries
const keys = Object.keys(obj);
const values = Object.values(obj);
const entries = Object.entries(obj);
// Spread
const copy = { ...obj, extra: true };
// delete
delete obj[key];
Destructuring
// Array destructuring
const [a, b, ...rest] = [1, 2, 3, 4, 5];
// Object destructuring
const { name, age, email = "none" } = user;
// Rename
const { name: userName } = user;
// Rest pattern
const { id, ...remaining } = obj;
// Function parameter destructuring
function process({ name, age }: User) {
console.log(name, age);
}
Template Literals
const name = "world";
const greeting = `Hello, ${name}!`;
const multiline = `
Line 1
Line 2
`;
const expr = `Result: ${1 + 2}`;
Spread and Rest
// Array spread
const combined = [...arr1, ...arr2];
// Object spread
const merged = { ...defaults, ...overrides };
// Rest parameters
function log(...args: any[]) { /* ... */ }
Closures
function makeCounter() {
let count = 0;
return {
increment: () => ++count,
get: () => count,
};
}
const counter = makeCounter();
counter.increment();
console.log(counter.get()); // 1
Perry performs closure conversion — captured variables are stored in heap-allocated closure objects.
Async/Await
async function fetchUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
return await response.json();
}
// Top-level await
const data = await fetchUser(1);
Perry compiles async functions to a state machine backed by Tokio’s async runtime.
Promises
const p = new Promise<number>((resolve, reject) => {
resolve(42);
});
p.then((value) => console.log(value));
// Promise.all
const results = await Promise.all([fetch(url1), fetch(url2)]);
Generators
function* range(start: number, end: number) {
for (let i = start; i < end; i++) {
yield i;
}
}
for (const n of range(0, 10)) {
console.log(n);
}
Map and Set
const map = new Map<string, number>();
map.set("a", 1);
map.get("a");
map.has("a");
map.delete("a");
map.size;
const set = new Set<number>();
set.add(1);
set.has(1);
set.delete(1);
set.size;
Regular Expressions
const re = /hello\s+(\w+)/;
const match = "hello world".match(re);
if (re.test("hello perry")) {
console.log("Matched!");
}
const replaced = "hello world".replace(/world/, "perry");
Error Handling
try {
throw new Error("something went wrong");
} catch (e) {
console.log(e.message);
} finally {
console.log("cleanup");
}
JSON
const obj = JSON.parse('{"key": "value"}');
const str = JSON.stringify(obj);
const pretty = JSON.stringify(obj, null, 2);
typeof and instanceof
if (typeof x === "string") {
console.log(x.length);
}
if (obj instanceof Dog) {
obj.speak();
}
typeof checks NaN-boxing tags at runtime. instanceof walks the class ID chain.
Modules
// Named exports
export function helper() { /* ... */ }
export const VALUE = 42;
// Default export
export default class MyClass { /* ... */ }
// Import
import MyClass, { helper, VALUE } from "./module";
import * as utils from "./utils";
// Re-exports
export { helper } from "./module";
BigInt
const big = BigInt(9007199254740991);
const result = big + BigInt(1);
// Bitwise operations
const and = big & BigInt(0xFF);
const or = big | BigInt(0xFF);
const xor = big ^ BigInt(0xFF);
const shl = big << BigInt(2);
const shr = big >> BigInt(2);
const not = ~big;
String Methods
const s = "Hello, World!";
s.length;
s.toUpperCase();
s.toLowerCase();
s.trim();
s.split(", ");
s.includes("World");
s.startsWith("Hello");
s.endsWith("!");
s.indexOf("World");
s.slice(0, 5);
s.substring(0, 5);
s.replace("World", "Perry");
s.repeat(3);
s.charAt(0);
s.padStart(20);
s.padEnd(20);
Math
Math.floor(3.7);
Math.ceil(3.2);
Math.round(3.5);
Math.abs(-5);
Math.max(1, 2, 3);
Math.min(1, 2, 3);
Math.sqrt(16);
Math.pow(2, 10);
Math.random();
Math.PI;
Math.E;
Math.log(10);
Math.sin(0);
Math.cos(0);
Date
const now = Date.now();
const d = new Date();
d.getTime();
d.toISOString();
Console
console.log("message");
console.error("error");
console.warn("warning");
console.time("label");
console.timeEnd("label");
JSX/TSX
Perry supports JSX syntax for UI component composition:
// Component functions
function Greeting({ name }: { name: string }) {
return <Text>{`Hello, ${name}!`}</Text>;
}
// JSX elements
<Button onClick={() => console.log("clicked")}>Click me</Button>
// Fragments
<>
<Text>Line 1</Text>
<Text>Line 2</Text>
</>
// Spread props
<Component {...props} extra="value" />
// Conditional rendering
{condition ? <Text>Yes</Text> : <Text>No</Text>}
JSX elements are transformed to function calls via the jsx()/jsxs() runtime.
Next Steps
- Type System — Type inference and checking
- Limitations — What’s not supported yet
Type System
Perry erases types at compile time, similar to how tsc removes type annotations when emitting JavaScript. However, Perry also performs type inference to generate efficient native code.
Type Inference
Perry infers types from expressions without requiring annotations:
let x = 5; // inferred as number
let s = "hello"; // inferred as string
let b = true; // inferred as boolean
let arr = [1, 2, 3]; // inferred as number[]
Inference works through:
- Literal values:
5→number,"hi"→string - Binary operations:
a + bwhere both are numbers →number - Variable propagation: if
xisnumber, thenlet y = xisnumber - Method returns:
"hello".trim()→string,[1,2].length→number - Function returns: user-defined function return types are propagated to callers
function double(n: number): number {
return n * 2;
}
let result = double(5); // inferred as number
Type Annotations
Standard TypeScript annotations work:
let name: string = "Perry";
let count: number = 0;
let items: string[] = [];
function greet(name: string): string {
return `Hello, ${name}`;
}
interface Config {
port: number;
host: string;
}
Utility Types
Common TypeScript utility types are erased at compile time (they don’t affect code generation):
type Partial<T> = { [P in keyof T]?: T[P] };
type Pick<T, K> = { [P in K]: T[P] };
type Record<K, V> = { [P in K]: V };
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type ReturnType<T> = /* ... */;
type Readonly<T> = { readonly [P in keyof T]: T[P] };
These are all recognized and erased — they won’t cause compilation errors.
Generics
Generic type parameters are erased:
function identity<T>(value: T): T {
return value;
}
class Box<T> {
value: T;
constructor(value: T) {
this.value = value;
}
}
const box = new Box<number>(42);
At runtime, all values are NaN-boxed — the generic parameter doesn’t affect code generation.
Type Checking with --type-check
For stricter type checking, Perry can integrate with Microsoft’s TypeScript checker:
perry file.ts --type-check
This resolves cross-file types, interfaces, and generics via an IPC protocol. It falls back gracefully if the type checker is not installed.
Without --type-check, Perry relies on its own inference engine, which handles common patterns but doesn’t perform full TypeScript type checking.
Union and Intersection Types
Union types are recognized syntactically but don’t affect code generation:
type StringOrNumber = string | number;
function process(value: StringOrNumber) {
if (typeof value === "string") {
console.log(value.toUpperCase());
} else {
console.log(value + 1);
}
}
Use typeof checks for runtime type narrowing.
Type Guards
function isString(value: any): value is string {
return typeof value === "string";
}
if (isString(x)) {
console.log(x.toUpperCase());
}
The value is string annotation is erased, but the typeof check works at runtime.
Next Steps
- Supported Features — Complete feature list
- Limitations — What’s not supported
Limitations
Perry compiles a practical subset of TypeScript. This page documents what’s not supported or works differently from Node.js/tsc.
No Runtime Type Checking
Types are erased at compile time. There is no runtime type system — Perry doesn’t generate type guards or runtime type metadata.
// These annotations are erased — no runtime effect
const x: number = someFunction(); // No runtime check that result is actually a number
Use explicit typeof checks where runtime type discrimination is needed.
No eval() or Dynamic Code
Perry compiles to native code ahead of time. Dynamic code execution is not possible:
// Not supported
eval("console.log('hi')");
new Function("return 42");
No Decorators
TypeScript decorators are not currently supported:
// Not supported
@Component
class MyClass {}
No Reflection
There is no Reflect API or runtime type metadata:
// Not supported
Reflect.getMetadata("design:type", target, key);
No Dynamic require()
Only static imports are supported:
// Supported
import { foo } from "./module";
// Not supported
const mod = require("./module");
const mod = await import("./module");
No Prototype Manipulation
Perry compiles classes to fixed structures. Dynamic prototype modification is not supported:
// Not supported
MyClass.prototype.newMethod = function() {};
Object.setPrototypeOf(obj, proto);
No Symbol Type
The Symbol primitive type is not currently supported:
// Not supported
const sym = Symbol("description");
No WeakMap/WeakRef
Weak references are not implemented:
// Not supported
const wm = new WeakMap();
const wr = new WeakRef(obj);
No Proxy
The Proxy object is not supported:
// Not supported
const proxy = new Proxy(target, handler);
Limited Error Types
Error and basic throw/catch work, but custom error subclasses have limited support:
// Works
throw new Error("message");
// Limited
class CustomError extends Error {
code: number;
constructor(msg: string, code: number) {
super(msg);
this.code = code;
}
}
Single-Threaded Execution
User code runs on a single thread. Async I/O runs on Tokio worker threads, but there’s no SharedArrayBuffer or true multi-threading for user code.
worker_threads is supported for background tasks, but workers communicate via message passing (not shared memory).
No Computed Property Names
Dynamic property keys in object literals are limited:
// Supported
const key = "name";
obj[key] = "value";
// Not supported
const obj = { [key]: "value" };
npm Package Compatibility
Not all npm packages work with Perry:
- Natively supported: ~50 popular packages (fastify, mysql2, redis, etc.) — these are compiled natively. See Standard Library.
compilePackages: Pure TS/JS packages can be compiled natively via configuration.- Not supported: Packages requiring native addons (
.nodefiles),eval(), dynamicrequire(), or Node.js internals.
Workarounds
Dynamic Behavior
For cases where you need dynamic behavior, use the JavaScript runtime fallback:
import { jsEval } from "perry/jsruntime";
// Routes specific code through QuickJS for dynamic evaluation
Type Narrowing
Since there’s no runtime type checking, use explicit checks:
// Instead of relying on type narrowing from generics
if (typeof value === "string") {
// String path
} else if (typeof value === "number") {
// Number path
}
Next Steps
- Supported Features — What does work
- Type System — How types are handled
UI Overview
Perry’s perry/ui module lets you build native desktop and mobile apps with declarative TypeScript. Your UI code compiles directly to platform-native widgets — AppKit on macOS, UIKit on iOS, GTK4 on Linux, Win32 on Windows, and DOM elements on the web.
Quick Start
import { App, Text, VStack } from "perry/ui";
App("My App", () =>
VStack([
Text("Hello from Perry!"),
])
);
perry app.ts -o app && ./app
App Lifecycle
Every Perry UI app starts with App():
import { App } from "perry/ui";
App("Window Title", () => {
// Build and return your widget tree
return VStack([
Text("Content here"),
]);
});
App(title, renderFn) creates the native application, opens a window, and renders your widget tree. The render function is called initially and re-invoked when reactive state changes.
Lifecycle Hooks
import { App, onActivate, onTerminate } from "perry/ui";
onActivate(() => {
console.log("App became active");
});
onTerminate(() => {
console.log("App is closing");
});
App("My App", () => { /* ... */ });
Widget Tree
Perry UIs are built as a tree of widgets:
import { App, Text, Button, VStack, HStack } from "perry/ui";
App("Layout Demo", () =>
VStack([
Text("Header"),
HStack([
Button("Left", () => console.log("left")),
Button("Right", () => console.log("right")),
]),
Text("Footer"),
])
);
Widgets are created by calling their constructor functions. Layout containers (VStack, HStack, ZStack) accept arrays of child widgets.
Handle-Based Architecture
Under the hood, each widget is a handle — a small integer that references a native platform object. When you call Text("hello"), Perry creates a native NSTextField (macOS), UILabel (iOS), GtkLabel (Linux), or <span> (web) and returns a handle you can use to modify it.
const label = Text("Hello");
label.setFontSize(18); // Modifies the native widget
label.setColor("#FF0000"); // Through the handle
Imports
All UI functions are imported from perry/ui:
import {
// App lifecycle
App, onActivate, onTerminate,
// Widgets
Text, Button, TextField, SecureField, Toggle, Slider,
Image, ProgressView, Picker,
// Layout
VStack, HStack, ZStack, ScrollView, Spacer, Divider,
NavigationStack, LazyVStack, Form, Section,
// State
State, ForEach,
// Dialogs
openFileDialog, saveFileDialog, alert, Sheet,
// Menus
menuBarCreate, menuBarAddMenu, contextMenu,
// Canvas
Canvas,
// Table
Table,
// Window
Window,
} from "perry/ui";
Platform Differences
The same code runs on all platforms, but the look and feel matches each platform’s native style:
| Feature | macOS | iOS | Linux | Windows | Web |
|---|---|---|---|---|---|
| Buttons | NSButton | UIButton | GtkButton | HWND Button | <button> |
| Text | NSTextField | UILabel | GtkLabel | Static HWND | <span> |
| Layout | NSStackView | UIStackView | GtkBox | Manual layout | Flexbox |
| Menus | NSMenu | — | GMenu | HMENU | DOM |
Platform-specific behavior is noted on each widget’s documentation page.
Next Steps
- Widgets — All available widgets
- Layout — Arranging widgets
- State Management — Reactive state and bindings
- Styling — Colors, fonts, sizing
- Events — Click, hover, keyboard
Widgets
Perry provides native widgets that map to each platform’s native controls.
Text
Displays read-only text.
import { Text } from "perry/ui";
const label = Text("Hello, World!");
label.setFontSize(18);
label.setColor("#333333");
label.setFontFamily("Menlo");
Methods:
setText(text: string)— Update the text contentsetFontSize(size: number)— Set font size in pointssetColor(hex: string)— Set text color (hex string)setFontFamily(family: string)— Set font family (e.g.,"Menlo"for monospaced)setAccessibilityLabel(label: string)— Set accessibility label
Text widgets inside template literals with State.get() update automatically:
const count = State(0);
Text(`Count: ${count.get()}`); // Updates when count changes
Button
A clickable button.
import { Button } from "perry/ui";
const btn = Button("Click Me", () => {
console.log("Clicked!");
});
btn.setCornerRadius(8);
btn.setBackgroundColor("#007AFF");
Methods:
setOnClick(callback: () => void)— Set click handlersetImage(sfSymbolName: string)— Set SF Symbol icon (macOS/iOS)setContentTintColor(hex: string)— Set tint colorsetImagePosition(position: number)— Set image position relative to textsetEnabled(enabled: boolean)— Enable/disable the buttonsetAccessibilityLabel(label: string)— Set accessibility label
TextField
An editable text input.
import { TextField, State } from "perry/ui";
const text = State("");
const field = TextField(text, "Placeholder...");
TextField takes a State for two-way binding — the state updates as the user types, and setting the state updates the field.
Methods:
setText(text: string)— Set the text programmaticallysetPlaceholder(text: string)— Set placeholder textsetEnabled(enabled: boolean)— Enable/disable editing
SecureField
A password input field (text is masked).
import { SecureField, State } from "perry/ui";
const password = State("");
SecureField(password, "Enter password...");
Same API as TextField but input is hidden.
Toggle
A boolean on/off switch.
import { Toggle, State } from "perry/ui";
const enabled = State(false);
Toggle("Enable notifications", enabled);
The State is bound two-way — toggling updates the state, and setting the state updates the toggle.
Slider
A numeric slider.
import { Slider, State } from "perry/ui";
const value = State(50);
Slider(value, 0, 100); // state, min, max
Picker
A dropdown selection control.
import { Picker, State } from "perry/ui";
const selected = State(0);
Picker(["Option A", "Option B", "Option C"], selected);
Image
Displays an image.
import { Image } from "perry/ui";
const img = Image("path/to/image.png");
img.setWidth(200);
img.setHeight(150);
On macOS/iOS, you can also use SF Symbol names:
Image("star.fill"); // SF Symbol
ProgressView
An indeterminate or determinate progress indicator.
import { ProgressView } from "perry/ui";
const progress = ProgressView();
// Or with a value (0.0 to 1.0)
const progress = ProgressView(0.5);
Form and Section
Group controls into a form layout.
import { Form, Section, TextField, Toggle, State } from "perry/ui";
const name = State("");
const notifications = State(true);
Form([
Section("Personal Info", [
TextField(name, "Name"),
]),
Section("Settings", [
Toggle("Notifications", notifications),
]),
]);
Table
A data table with rows and columns.
import { Table } from "perry/ui";
const table = Table(10, 3, (row, col) => {
return `Cell ${row},${col}`;
});
table.setColumnHeader(0, "Name");
table.setColumnHeader(1, "Email");
table.setColumnHeader(2, "Role");
table.setColumnWidth(0, 200);
table.setColumnWidth(1, 250);
table.setOnRowSelect((row) => {
console.log(`Selected row: ${row}`);
});
Methods:
setColumnHeader(col: number, title: string)— Set column header textsetColumnWidth(col: number, width: number)— Set column widthupdateRowCount(count: number)— Update the number of rowssetOnRowSelect(callback: (row: number) => void)— Row selection handlergetSelectedRow()— Get currently selected row index
TextArea
A multi-line text input.
import { TextArea, State } from "perry/ui";
const content = State("");
TextArea(content, "Enter text...");
Methods:
setText(text: string)— Set the text programmaticallygetText()— Get the current text
QRCode
Generates and displays a QR code.
import { QRCode } from "perry/ui";
const qr = QRCode("https://example.com", 200); // data, size
qr.setData("https://other-url.com"); // Update data
Canvas
A drawing surface. See Canvas for the full drawing API.
import { Canvas } from "perry/ui";
const canvas = Canvas(400, 300, (ctx) => {
ctx.fillRect(10, 10, 100, 100);
ctx.strokeRect(50, 50, 100, 100);
});
Common Widget Methods
All widgets support these methods:
| Method | Description |
|---|---|
setWidth(width) | Set width |
setHeight(height) | Set height |
setBackgroundColor(hex) | Set background color |
setCornerRadius(radius) | Set corner radius |
setOpacity(alpha) | Set opacity (0.0–1.0) |
setEnabled(enabled) | Enable/disable interaction |
setHidden(hidden) | Show/hide widget |
setTooltip(text) | Set tooltip text |
setOnClick(callback) | Set click handler |
setOnHover(callback) | Set hover handler |
setOnDoubleClick(callback) | Set double-click handler |
See Styling and Events for complete details.
Next Steps
- Layout — Arranging widgets with stacks and containers
- Styling — Colors, fonts, borders
- State Management — Reactive bindings
Layout
Perry provides layout containers that arrange child widgets using the platform’s native layout system.
VStack
Arranges children vertically (top to bottom).
import { VStack, Text, Button } from "perry/ui";
VStack([
Text("First"),
Text("Second"),
Text("Third"),
]);
Methods:
setPadding(padding: number)— Set padding around all edgessetSpacing(spacing: number)— Set spacing between children
HStack
Arranges children horizontally (left to right).
import { HStack, Text, Button } from "perry/ui";
HStack([
Button("Cancel", () => {}),
Spacer(),
Button("OK", () => {}),
]);
ZStack
Layers children on top of each other (back to front).
import { ZStack, Text, Image } from "perry/ui";
ZStack([
Image("background.png"),
Text("Overlay text"),
]);
ScrollView
A scrollable container.
import { ScrollView, VStack, Text } from "perry/ui";
ScrollView(
VStack(
Array.from({ length: 100 }, (_, i) => Text(`Row ${i}`))
)
);
Methods:
setRefreshControl(callback: () => void)— Add pull-to-refresh (calls callback on pull)endRefreshing()— Stop the refresh indicator
LazyVStack
A vertically scrolling list that lazily renders items. More efficient than ScrollView + VStack for large lists.
import { LazyVStack, Text } from "perry/ui";
LazyVStack(1000, (index) => {
return Text(`Row ${index}`);
});
NavigationStack
A navigation container that supports push/pop navigation.
import { NavigationStack, Text, Button } from "perry/ui";
NavigationStack([
Text("Home Screen"),
Button("Go to Details", () => {
// Push a new view
}),
]);
Spacer
A flexible space that expands to fill available room.
import { HStack, Text, Spacer } from "perry/ui";
HStack([
Text("Left"),
Spacer(),
Text("Right"),
]);
Use Spacer() inside HStack or VStack to push widgets apart.
Divider
A visual separator line.
import { VStack, Text, Divider } from "perry/ui";
VStack([
Text("Section 1"),
Divider(),
Text("Section 2"),
]);
Nesting Layouts
Layouts can be nested freely:
import { App, VStack, HStack, Text, Button, Spacer, Divider } from "perry/ui";
App("Layout Example", () =>
VStack([
// Header
HStack([
Text("My App"),
Spacer(),
Button("Settings", () => {}),
]),
Divider(),
// Content
VStack([
Text("Welcome!"),
HStack([
Button("Action 1", () => {}),
Button("Action 2", () => {}),
]),
]),
Spacer(),
// Footer
Text("v1.0.0"),
])
);
Child Management
Containers support dynamic child management:
const stack = VStack([]);
// Add children dynamically
stack.addChild(Text("New child"));
stack.addChildAt(0, Text("Prepended"));
stack.removeChild(someWidget);
stack.reorderChild(widget, 2);
stack.clearChildren();
Methods:
addChild(widget)— Append a child widgetaddChildAt(index, widget)— Insert a child at a specific positionremoveChild(widget)— Remove a child widgetreorderChild(widget, newIndex)— Move a child to a new positionclearChildren()— Remove all children
Next Steps
- Styling — Colors, padding, sizing
- Widgets — All available widgets
- State Management — Dynamic UI with state
Styling
Perry widgets support native styling properties that map to each platform’s styling system.
Colors
import { Text, Button } from "perry/ui";
const label = Text("Colored text");
label.setColor("#FF0000"); // Text color (hex)
label.setBackgroundColor("#F0F0F0"); // Background color
Colors are specified as hex strings (#RRGGBB).
Fonts
const label = Text("Styled text");
label.setFontSize(24); // Font size in points
label.setFontFamily("Menlo"); // Font family name
Use "monospaced" for the system monospaced font.
Corner Radius
const btn = Button("Rounded", () => {});
btn.setCornerRadius(12);
Borders
const widget = VStack([]);
widget.setBorderColor("#CCCCCC");
widget.setBorderWidth(1);
Padding and Insets
const stack = VStack([Text("Padded content")]);
stack.setPadding(16);
stack.setEdgeInsets(10, 20, 10, 20); // top, right, bottom, left
Sizing
const widget = VStack([]);
widget.setWidth(300);
widget.setHeight(200);
widget.setFrame(0, 0, 300, 200); // x, y, width, height
Opacity
const widget = Text("Semi-transparent");
widget.setOpacity(0.5); // 0.0 to 1.0
Background Gradient
const widget = VStack([]);
widget.setBackgroundGradient("#FF0000", "#0000FF"); // Start color, end color
Control Size
const btn = Button("Small", () => {});
btn.setControlSize(0); // 0=mini, 1=small, 2=regular, 3=large
macOS: Maps to
NSControl.ControlSize. Other platforms may interpret differently.
Tooltips
const btn = Button("Hover me", () => {});
btn.setTooltip("Click to perform action");
macOS/Windows/Linux: Native tooltips. iOS/Android: No tooltip support. Web: HTML
titleattribute.
Enabled/Disabled
const btn = Button("Submit", () => {});
btn.setEnabled(false); // Greys out and disables interaction
Complete Styling Example
import { App, Text, Button, VStack, HStack, State, Spacer } from "perry/ui";
const count = State(0);
App("Styled App", () => {
const title = Text("Counter");
title.setFontSize(28);
title.setColor("#1A1A1A");
const display = Text(`${count.get()}`);
display.setFontSize(48);
display.setFontFamily("monospaced");
display.setColor("#007AFF");
const decBtn = Button("-", () => count.set(count.get() - 1));
decBtn.setCornerRadius(20);
decBtn.setBackgroundColor("#FF3B30");
const incBtn = Button("+", () => count.set(count.get() + 1));
incBtn.setCornerRadius(20);
incBtn.setBackgroundColor("#34C759");
const controls = HStack([decBtn, Spacer(), incBtn]);
controls.setPadding(20);
const container = VStack([title, display, controls]);
container.setPadding(40);
container.setCornerRadius(16);
container.setBackgroundColor("#FFFFFF");
container.setBorderColor("#E5E5E5");
container.setBorderWidth(1);
return container;
});
Next Steps
State Management
Perry uses reactive state to automatically update the UI when data changes.
Creating State
import { State } from "perry/ui";
const count = State(0); // number state
const name = State("Perry"); // string state
const items = State<string[]>([]); // array state
State(initialValue) creates a reactive state container.
Reading and Writing
const value = count.get(); // Read current value
count.set(42); // Set new value → triggers UI update
Every .set() call re-renders the widget tree with the new value.
Reactive Text
Template literals with state.get() update automatically:
import { Text, State } from "perry/ui";
const count = State(0);
Text(`Count: ${count.get()}`);
// The text updates whenever count changes
This works because Perry detects State.get() calls inside template literals and creates reactive bindings.
Two-Way Binding
TextField and other input widgets bind to state bidirectionally:
import { TextField, State } from "perry/ui";
const input = State("");
TextField(input, "Type here...");
// input.get() always reflects what the user typed
// input.set("hello") updates the text field
Controls that support two-way binding:
TextField(state, placeholder)— text inputSecureField(state, placeholder)— password inputToggle(label, state)— boolean toggleSlider(state, min, max)— numeric sliderPicker(options, state)— selection
onChange Callbacks
Listen for state changes:
import { State } from "perry/ui";
const count = State(0);
count.onChange((newValue) => {
console.log(`Count changed to ${newValue}`);
});
ForEach
Render a list from array state:
import { VStack, Text, ForEach, State } from "perry/ui";
const items = State(["Apple", "Banana", "Cherry"]);
VStack([
ForEach(items, (item, index) =>
Text(`${index + 1}. ${item}`)
),
]);
ForEach re-renders the list when the state changes:
// Add an item
items.set([...items.get(), "Date"]);
// Remove an item
items.set(items.get().filter((_, i) => i !== 1));
Conditional Rendering
Use state to conditionally show widgets:
import { VStack, Text, Button, State } from "perry/ui";
const showDetails = State(false);
VStack([
Button("Toggle", () => showDetails.set(!showDetails.get())),
showDetails.get() ? Text("Details are visible!") : Spacer(),
]);
Multi-State Text
Text can depend on multiple state values:
const firstName = State("John");
const lastName = State("Doe");
Text(`Hello, ${firstName.get()} ${lastName.get()}!`);
// Updates when either firstName or lastName changes
State with Objects and Arrays
const user = State({ name: "Perry", age: 0 });
// Update by replacing the whole object
user.set({ ...user.get(), age: 1 });
const todos = State<{ text: string; done: boolean }[]>([]);
// Add a todo
todos.set([...todos.get(), { text: "New task", done: false }]);
// Toggle a todo
const items = todos.get();
items[0].done = !items[0].done;
todos.set([...items]);
Note: State uses identity comparison. You must create a new array/object reference for changes to be detected. Mutating in-place without calling
.set()with a new reference won’t trigger updates.
Complete Example
import { App, Text, Button, TextField, VStack, HStack, State, ForEach, Spacer, Divider } from "perry/ui";
const todos = State<string[]>([]);
const input = State("");
App("Todo App", () =>
VStack([
Text("My Todos"),
HStack([
TextField(input, "What needs to be done?"),
Button("Add", () => {
const text = input.get();
if (text.length > 0) {
todos.set([...todos.get(), text]);
input.set("");
}
}),
]),
Divider(),
ForEach(todos, (todo, index) =>
HStack([
Text(todo),
Spacer(),
Button("Delete", () => {
todos.set(todos.get().filter((_, i) => i !== index));
}),
])
),
Spacer(),
Text(`${todos.get().length} items`),
])
);
Next Steps
Events
Perry widgets support native event handlers for user interaction.
onClick
import { Button, Text } from "perry/ui";
Button("Click me", () => {
console.log("Button clicked!");
});
// Or set it after creation
const label = Text("Clickable text");
label.setOnClick(() => {
console.log("Text clicked!");
});
onHover
Triggered when the mouse enters or leaves a widget.
import { Button } from "perry/ui";
const btn = Button("Hover me", () => {});
btn.setOnHover((isHovering) => {
if (isHovering) {
console.log("Mouse entered");
} else {
console.log("Mouse left");
}
});
Note: Hover events are available on macOS, Windows, Linux, and Web. iOS and Android use touch interactions instead.
onDoubleClick
import { Text } from "perry/ui";
const label = Text("Double-click me");
label.setOnDoubleClick(() => {
console.log("Double-clicked!");
});
Keyboard Shortcuts
Register global keyboard shortcuts:
import { registerShortcut } from "perry/ui";
// Cmd+N on macOS, Ctrl+N on other platforms
registerShortcut("n", () => {
console.log("New document");
});
// Cmd+Shift+S
registerShortcut("S", () => {
console.log("Save as...");
});
Keyboard shortcuts are also supported in menu items:
menuAddItem(menu, "New", () => newDoc(), "n"); // Cmd+N
menuAddItem(menu, "Save As", () => saveAs(), "S"); // Cmd+Shift+S
Clipboard
import { clipboardGet, clipboardSet } from "perry/ui";
// Copy to clipboard
clipboardSet("Hello, clipboard!");
// Read from clipboard
const text = clipboardGet();
Complete Example
import { App, Text, Button, VStack, HStack, State, Spacer, registerShortcut } from "perry/ui";
const lastEvent = State("No events yet");
// Global shortcut
registerShortcut("r", () => {
lastEvent.set("Keyboard: Cmd+R");
});
App("Events Demo", () =>
VStack([
Text(`Last event: ${lastEvent.get()}`),
Spacer(),
Button("Click me", () => {
lastEvent.set("Button clicked");
}),
(() => {
const hoverBtn = Button("Hover me", () => {});
hoverBtn.setOnHover((h) => {
lastEvent.set(h ? "Mouse entered" : "Mouse left");
});
return hoverBtn;
})(),
(() => {
const dblLabel = Text("Double-click me");
dblLabel.setOnDoubleClick(() => {
lastEvent.set("Double-clicked!");
});
return dblLabel;
})(),
])
);
Next Steps
- Menus — Menu bar and context menus with keyboard shortcuts
- Widgets — All available widgets
- State Management — Reactive state
Canvas
The Canvas widget provides a 2D drawing surface for custom graphics.
Creating a Canvas
import { Canvas } from "perry/ui";
const canvas = Canvas(400, 300, (ctx) => {
// Drawing code here
ctx.fillRect(10, 10, 100, 80);
});
Canvas(width, height, drawCallback) creates a canvas and calls your drawing function.
Drawing Shapes
Rectangles
Canvas(400, 300, (ctx) => {
// Filled rectangle
ctx.setFillColor("#FF0000");
ctx.fillRect(10, 10, 100, 80);
// Stroked rectangle
ctx.setStrokeColor("#0000FF");
ctx.setLineWidth(2);
ctx.strokeRect(150, 10, 100, 80);
});
Lines
Canvas(400, 300, (ctx) => {
ctx.setStrokeColor("#000000");
ctx.setLineWidth(1);
ctx.beginPath();
ctx.moveTo(10, 10);
ctx.lineTo(200, 150);
ctx.stroke();
});
Circles and Arcs
Canvas(400, 300, (ctx) => {
ctx.setFillColor("#00FF00");
ctx.beginPath();
ctx.arc(200, 150, 50, 0, Math.PI * 2); // x, y, radius, startAngle, endAngle
ctx.fill();
});
Colors
Canvas(400, 300, (ctx) => {
ctx.setFillColor("#FF6600"); // Hex color
ctx.setStrokeColor("#333333");
ctx.setLineWidth(3);
});
Gradients
Canvas(400, 300, (ctx) => {
ctx.setGradient("#FF0000", "#0000FF"); // Start color, end color
ctx.fillRect(0, 0, 400, 300);
});
Text on Canvas
Canvas(400, 300, (ctx) => {
ctx.setFillColor("#000000");
ctx.fillText("Hello Canvas!", 50, 50);
});
Platform Notes
| Platform | Implementation |
|---|---|
| macOS | Core Graphics (CGContext) |
| iOS | Core Graphics (CGContext) |
| Linux | Cairo |
| Windows | GDI |
| Android | Canvas/Bitmap |
| Web | HTML5 Canvas |
Complete Example
import { App, Canvas, VStack } from "perry/ui";
App("Canvas Demo", () =>
VStack([
Canvas(400, 300, (ctx) => {
// Background
ctx.setFillColor("#1A1A2E");
ctx.fillRect(0, 0, 400, 300);
// Sun
ctx.setFillColor("#FFD700");
ctx.beginPath();
ctx.arc(300, 80, 40, 0, Math.PI * 2);
ctx.fill();
// Ground
ctx.setFillColor("#2D5016");
ctx.fillRect(0, 220, 400, 80);
// Tree trunk
ctx.setFillColor("#8B4513");
ctx.fillRect(80, 150, 20, 70);
// Tree top
ctx.setFillColor("#228B22");
ctx.beginPath();
ctx.arc(90, 130, 40, 0, Math.PI * 2);
ctx.fill();
}),
])
);
Next Steps
Menus
Perry supports native menu bars, context menus, and toolbar items across all platforms.
Menu Bar
Create a native application menu bar:
import { App, menuBarCreate, menuBarAddMenu, menuAddItem, menuAddSeparator, menuAddSubmenu, menuBarAttach } from "perry/ui";
App("Menu Demo", () => {
const menuBar = menuBarCreate();
// File menu
const fileMenu = menuBarAddMenu(menuBar, "File");
menuAddItem(fileMenu, "New", () => newDoc(), "n"); // Cmd+N
menuAddItem(fileMenu, "Open", () => openDoc(), "o"); // Cmd+O
menuAddSeparator(fileMenu);
menuAddItem(fileMenu, "Save", () => saveDoc(), "s"); // Cmd+S
menuAddItem(fileMenu, "Save As...", () => saveAs(), "S"); // Cmd+Shift+S
// Edit menu
const editMenu = menuBarAddMenu(menuBar, "Edit");
menuAddItem(editMenu, "Undo", () => undo(), "z");
menuAddItem(editMenu, "Redo", () => redo(), "Z"); // Cmd+Shift+Z
menuAddSeparator(editMenu);
menuAddItem(editMenu, "Cut", () => cut(), "x");
menuAddItem(editMenu, "Copy", () => copy(), "c");
menuAddItem(editMenu, "Paste", () => paste(), "v");
// Submenu
const viewMenu = menuBarAddMenu(menuBar, "View");
const zoomSubmenu = menuAddSubmenu(viewMenu, "Zoom");
menuAddItem(zoomSubmenu, "Zoom In", () => zoomIn(), "+");
menuAddItem(zoomSubmenu, "Zoom Out", () => zoomOut(), "-");
menuAddItem(zoomSubmenu, "Actual Size", () => zoomReset(), "0");
menuBarAttach(menuBar);
// ... rest of UI
});
Menu Bar Functions
menuBarCreate()— Create a new menu barmenuBarAddMenu(menuBar, title)— Add a top-level menu, returns menu handlemenuAddItem(menu, label, callback, shortcut?)— Add a menu item with optional keyboard shortcutmenuAddSeparator(menu)— Add a separator linemenuAddSubmenu(menu, title)— Add a submenu, returns submenu handlemenuBarAttach(menuBar)— Attach the menu bar to the application
Keyboard Shortcuts
The 4th argument to menuAddItem is an optional keyboard shortcut:
| Shortcut | macOS | Other |
|---|---|---|
"n" | Cmd+N | Ctrl+N |
"S" | Cmd+Shift+S | Ctrl+Shift+S |
"+" | Cmd++ | Ctrl++ |
Uppercase letters imply Shift.
Context Menus
Right-click menus on widgets:
import { Text, contextMenu } from "perry/ui";
const label = Text("Right-click me");
contextMenu(label, [
{ label: "Copy", action: () => copyText() },
{ label: "Paste", action: () => pasteText() },
{ separator: true },
{ label: "Delete", action: () => deleteItem() },
]);
Toolbar
Add a toolbar to the window:
import { App, toolbarCreate, toolbarAddItem } from "perry/ui";
App("Toolbar Demo", () => {
const toolbar = toolbarCreate();
toolbarAddItem(toolbar, "New", () => newDoc());
toolbarAddItem(toolbar, "Save", () => saveDoc());
toolbarAddItem(toolbar, "Run", () => runCode());
// ... rest of UI
});
Platform Notes
| Platform | Menu Bar | Context Menu | Toolbar |
|---|---|---|---|
| macOS | NSMenu | NSMenu | NSToolbar |
| iOS | — (no menu bar) | UIMenu | UIToolbar |
| Windows | HMENU/SetMenu | — | Horizontal layout |
| Linux | GMenu/set_menubar | — | HeaderBar |
| Web | DOM | DOM | DOM |
iOS: Menu bars are not applicable. Use toolbar and navigation patterns instead.
Next Steps
- Events — Keyboard shortcuts and interactions
- Dialogs — File dialogs and alerts
- Toolbar and navigation
Dialogs
Perry provides native dialog functions for file selection, alerts, and sheets.
File Open Dialog
import { openFileDialog } from "perry/ui";
const filePath = openFileDialog();
if (filePath) {
console.log(`Selected: ${filePath}`);
}
Returns the selected file path, or null if cancelled.
Folder Selection Dialog
import { openFolderDialog } from "perry/ui";
const folderPath = openFolderDialog();
if (folderPath) {
console.log(`Selected folder: ${folderPath}`);
}
Save File Dialog
import { saveFileDialog } from "perry/ui";
const savePath = saveFileDialog();
if (savePath) {
// Write file to savePath
}
Alert
Display a native alert dialog:
import { alert } from "perry/ui";
alert("Operation Complete", "Your file has been saved successfully.");
alert(title, message) shows a modal alert with an OK button.
Sheets
Sheets are modal panels attached to a window:
import { Sheet, Text, Button, VStack } from "perry/ui";
const sheet = Sheet(() =>
VStack([
Text("Sheet Content"),
Button("Close", () => {
sheet.dismiss();
}),
])
);
// Show the sheet
sheet.present();
Platform Notes
| Dialog | macOS | iOS | Windows | Linux | Web |
|---|---|---|---|---|---|
| File Open | NSOpenPanel | UIDocumentPicker | IFileOpenDialog | GtkFileChooserDialog | <input type="file"> |
| File Save | NSSavePanel | — | IFileSaveDialog | GtkFileChooserDialog | Download link |
| Folder | NSOpenPanel | — | IFileOpenDialog | GtkFileChooserDialog | — |
| Alert | NSAlert | UIAlertController | MessageBoxW | MessageDialog | alert() |
| Sheet | NSSheet | Modal VC | Modal Dialog | Modal Window | Modal div |
Complete Example
import { App, Text, Button, VStack, HStack, State, openFileDialog, saveFileDialog, alert } from "perry/ui";
import { readFileSync, writeFileSync } from "perry/fs";
const content = State("");
const filePath = State("");
App("Text Editor", () =>
VStack([
HStack([
Button("Open", () => {
const path = openFileDialog();
if (path) {
filePath.set(path);
content.set(readFileSync(path));
}
}),
Button("Save As", () => {
const path = saveFileDialog();
if (path) {
writeFileSync(path, content.get());
filePath.set(path);
alert("Saved", `File saved to ${path}`);
}
}),
]),
Text(`File: ${filePath.get() || "No file open"}`),
TextField(content, "Start typing..."),
])
);
Next Steps
- Menus — Menu bar and context menus
- Multi-Window — Multiple windows
- Events — User interaction events
Table
The Table widget displays tabular data with columns, headers, and row selection.
Creating a Table
import { Table } from "perry/ui";
const table = Table(10, 3, (row, col) => {
return `Row ${row}, Col ${col}`;
});
Table(rowCount, colCount, renderCell) creates a table. The render function is called for each cell and should return the text content.
Column Headers
const table = Table(100, 3, (row, col) => {
const data = [
["Alice", "alice@example.com", "Admin"],
["Bob", "bob@example.com", "User"],
// ...
];
return data[row]?.[col] ?? "";
});
table.setColumnHeader(0, "Name");
table.setColumnHeader(1, "Email");
table.setColumnHeader(2, "Role");
Column Widths
table.setColumnWidth(0, 150); // Name column
table.setColumnWidth(1, 250); // Email column
table.setColumnWidth(2, 100); // Role column
Row Selection
table.setOnRowSelect((row) => {
console.log(`Selected row: ${row}`);
});
// Get the currently selected row
const selected = table.getSelectedRow();
Dynamic Row Count
Update the number of rows after creation:
table.updateRowCount(newCount);
Platform Notes
| Platform | Implementation |
|---|---|
| macOS | NSTableView + NSScrollView |
| Web | HTML <table> |
| iOS/Android/Linux/Windows | Stubs (pending native implementation) |
Complete Example
import { App, Table, Text, VStack, State } from "perry/ui";
const selectedName = State("None");
const users = [
{ name: "Alice", email: "alice@example.com", role: "Admin" },
{ name: "Bob", email: "bob@example.com", role: "Editor" },
{ name: "Charlie", email: "charlie@example.com", role: "Viewer" },
{ name: "Diana", email: "diana@example.com", role: "Admin" },
{ name: "Eve", email: "eve@example.com", role: "Editor" },
];
App("Table Demo", () =>
VStack([
(() => {
const table = Table(users.length, 3, (row, col) => {
const user = users[row];
if (col === 0) return user.name;
if (col === 1) return user.email;
return user.role;
});
table.setColumnHeader(0, "Name");
table.setColumnHeader(1, "Email");
table.setColumnHeader(2, "Role");
table.setColumnWidth(0, 150);
table.setColumnWidth(1, 250);
table.setColumnWidth(2, 100);
table.setOnRowSelect((row) => {
selectedName.set(users[row].name);
});
return table;
})(),
Text(`Selected: ${selectedName.get()}`),
])
);
Next Steps
Animation
Perry supports animating widget properties for smooth transitions.
Opacity Animation
import { Text } from "perry/ui";
const label = Text("Fading text");
// Animate opacity from current to target over duration
label.animateOpacity(0.0, 1.0); // targetOpacity, durationSeconds
Position Animation
import { Button } from "perry/ui";
const btn = Button("Moving", () => {});
// Animate position
btn.animatePosition(100, 200, 0.5); // targetX, targetY, durationSeconds
Example: Fade-In Effect
import { App, Text, Button, VStack, State } from "perry/ui";
const visible = State(false);
App("Animation Demo", () =>
VStack([
Button("Toggle", () => {
visible.set(!visible.get());
}),
(() => {
const label = Text("Hello!");
label.animateOpacity(visible.get() ? 1.0 : 0.0, 0.3);
return label;
})(),
])
);
Platform Notes
| Platform | Implementation |
|---|---|
| macOS | NSAnimationContext / ViewPropertyAnimator |
| iOS | UIView.animate |
| Android | ViewPropertyAnimator |
| Windows | WM_TIMER-based animation |
| Linux | CSS transitions (GTK4) |
| Web | CSS transitions |
Next Steps
Multi-Window
Perry supports creating multiple native windows in a single application.
Creating Windows
import { App, Window, Text, Button, VStack } from "perry/ui";
App("Multi-Window App", () =>
VStack([
Text("Main Window"),
Button("Open New Window", () => {
Window("Second Window", () =>
VStack([
Text("This is a second window"),
Button("Close", () => {
// Close this window
}),
])
);
}),
])
);
Window(title, renderFn) creates a new native window with its own widget tree.
Platform Notes
| Platform | Implementation |
|---|---|
| macOS | NSWindow |
| Windows | CreateWindowEx |
| Linux | GtkWindow |
| Web | Floating <div> |
| iOS/Android | Modal view controller / Dialog |
On mobile platforms, “windows” are presented as modal views or dialogs since mobile apps typically use a single-window model.
Next Steps
- Dialogs — Modal dialogs and sheets
- Menus — Menu bar and toolbar
- UI Overview — Full UI system overview
Platform Overview
Perry compiles TypeScript to native executables for 6 platforms from the same source code.
Supported Platforms
| Platform | Target Flag | UI Toolkit | Status |
|---|---|---|---|
| macOS | (default) | AppKit | Full support (127/127 FFI functions) |
| iOS | --target ios / --target ios-simulator | UIKit | Full support (127/127) |
| Android | --target android | JNI/Android SDK | Full support (112/112) |
| Windows | --target windows | Win32 | Full support (112/112) |
| Linux | --target linux | GTK4 | Full support (112/112) |
| Web | --target web | DOM/CSS | Full support (127/127) |
Cross-Compilation
# Default: compile for current platform
perry app.ts -o app
# Compile for a specific target
perry app.ts -o app --target ios-simulator
perry app.ts -o app --target web
perry app.ts -o app --target windows
perry app.ts -o app --target linux
perry app.ts -o app --target android
Platform Detection
Use the __platform__ compile-time constant to branch by platform:
declare const __platform__: number;
// Platform constants:
// 0 = macOS
// 1 = iOS
// 2 = Android
// 3 = Windows
// 4 = Linux
if (__platform__ === 0) {
console.log("Running on macOS");
} else if (__platform__ === 1) {
console.log("Running on iOS");
} else if (__platform__ === 3) {
console.log("Running on Windows");
}
__platform__ is resolved at compile time. The compiler constant-folds comparisons and eliminates dead branches, so platform-specific code has zero runtime cost.
Platform Feature Matrix
| Feature | macOS | iOS | Android | Windows | Linux | Web |
|---|---|---|---|---|---|---|
| CLI programs | Yes | — | — | Yes | Yes | — |
| Native UI | Yes | Yes | Yes | Yes | Yes | Yes |
| File system | Yes | Sandboxed | Sandboxed | Yes | Yes | — |
| Networking | Yes | Yes | Yes | Yes | Yes | Fetch |
| System APIs | Yes | Partial | Partial | Yes | Yes | Partial |
| Widgets (WidgetKit) | — | Yes | — | — | — | — |
Next Steps
macOS
macOS is Perry’s primary development platform. It uses AppKit for native UI.
Requirements
- macOS 13+ (Ventura or later)
- Xcode Command Line Tools:
xcode-select --install
Building
# macOS is the default target
perry app.ts -o app
./app
No additional flags needed — macOS is the default compilation target.
UI Toolkit
Perry maps UI widgets to AppKit controls:
| Perry Widget | AppKit Class |
|---|---|
| Text | NSTextField (label mode) |
| Button | NSButton |
| TextField | NSTextField |
| SecureField | NSSecureTextField |
| Toggle | NSSwitch |
| Slider | NSSlider |
| Picker | NSPopUpButton |
| Image | NSImageView |
| VStack/HStack | NSStackView |
| ScrollView | NSScrollView |
| Table | NSTableView |
| Canvas | NSView + Core Graphics |
Code Signing
For distribution, apps need to be signed. Perry supports automatic signing:
perry publish
This auto-detects your signing identity from the macOS Keychain, exports it to a temporary .p12 file, and signs the binary.
For manual signing:
codesign --sign "Developer ID Application: Your Name" ./app
App Store Distribution
perry app.ts -o MyApp
# Sign with App Store certificate
codesign --sign "3rd Party Mac Developer Application: Your Name" MyApp
# Package
productbuild --sign "3rd Party Mac Developer Installer: Your Name" --component MyApp /Applications MyApp.pkg
macOS-Specific Features
- Menu bar: Full NSMenu support with keyboard shortcuts
- Toolbar: NSToolbar integration
- Dock icon: Automatic for GUI apps
- Dark mode:
isDarkMode()detects system appearance - Keychain: Secure storage via Security.framework
- Notifications: Local notifications via UNUserNotificationCenter
- File dialogs: NSOpenPanel/NSSavePanel
System APIs
import { openURL, isDarkMode, preferencesSet, preferencesGet } from "perry/system";
openURL("https://example.com"); // Opens in default browser
const dark = isDarkMode(); // Check appearance
preferencesSet("key", "value"); // NSUserDefaults
const val = preferencesGet("key"); // NSUserDefaults
Next Steps
- iOS — Cross-compile for iPhone/iPad
- UI Overview — Full UI documentation
- System APIs — System integration
iOS
Perry can cross-compile TypeScript apps for iOS devices and the iOS Simulator.
Requirements
- macOS host (cross-compilation from Linux/Windows is not supported)
- Xcode (full install, not just Command Line Tools) for iOS SDK and Simulator
- Rust iOS targets:
rustup target add aarch64-apple-ios aarch64-apple-ios-sim
Building for Simulator
perry app.ts -o app --target ios-simulator
This uses Cranelift cross-compilation with the iOS Simulator SDK. The binary can be run in the Xcode Simulator.
Building for Device
perry app.ts -o app --target ios
This produces an ARM64 binary for physical iOS devices. You’ll need to code sign and package it in an .app bundle for deployment.
UI Toolkit
Perry maps UI widgets to UIKit controls:
| Perry Widget | UIKit Class |
|---|---|
| Text | UILabel |
| Button | UIButton (TouchUpInside) |
| TextField | UITextField |
| SecureField | UITextField (secureTextEntry) |
| Toggle | UISwitch |
| Slider | UISlider (Float32, cast at boundary) |
| Picker | UIPickerView |
| Image | UIImageView |
| VStack/HStack | UIStackView |
| ScrollView | UIScrollView |
App Lifecycle
iOS apps use UIApplicationMain with a deferred creation pattern:
import { App, Text, VStack } from "perry/ui";
App("My iOS App", () =>
VStack([
Text("Hello, iPhone!"),
])
);
The App() call triggers UIApplicationMain, and your render function is called via PerryAppDelegate once the app is ready.
iOS Widgets (WidgetKit)
Perry can compile TypeScript widget declarations to native SwiftUI WidgetKit extensions:
perry widget.ts --target ios-widget
See Widgets (WidgetKit) for details.
Differences from macOS
- No menu bar: iOS doesn’t support menu bars. Use toolbar or navigation patterns.
- Touch events:
onHoveris not available. UseonClick(mapped to touch). - Slider precision: iOS UISlider uses Float32 internally (automatically converted).
- File dialogs: Limited to UIDocumentPicker.
- Keyboard shortcuts: Not applicable on iOS.
Next Steps
- Widgets (WidgetKit) — iOS home screen widgets
- Platform Overview — All platforms
- UI Overview — UI system
Android
Perry compiles TypeScript apps for Android using JNI (Java Native Interface).
Requirements
- Android NDK
- Android SDK
- Rust Android targets:
rustup target add aarch64-linux-android armv7-linux-androideabi
Building
perry app.ts -o app --target android
UI Toolkit
Perry maps UI widgets to Android views via JNI:
| Perry Widget | Android Class |
|---|---|
| Text | TextView |
| Button | Button |
| TextField | EditText |
| SecureField | EditText (ES_PASSWORD) |
| Toggle | Switch |
| Slider | SeekBar |
| Picker | Spinner + ArrayAdapter |
| Image | ImageView |
| VStack | LinearLayout (vertical) |
| HStack | LinearLayout (horizontal) |
| ZStack | FrameLayout |
| ScrollView | ScrollView |
| Canvas | Canvas + Bitmap |
| NavigationStack | FrameLayout |
Android-Specific APIs
- Dark mode:
Configuration.uiModedetection - Preferences: SharedPreferences
- Keychain: Android Keystore
- Notifications: NotificationManager
- Open URL:
Intent.ACTION_VIEW - Alerts:
PerryBridge.showAlert - Sheets: Dialog (modal)
Differences from Desktop
- Touch-only: No hover events, no right-click context menus
- Single window: Multi-window maps to Dialog views
- Toolbar: Horizontal LinearLayout
- Font: Typeface-based font family support
Next Steps
- Platform Overview — All platforms
- UI Overview — UI system
Windows
Perry compiles TypeScript apps for Windows using the Win32 API.
Requirements
- Visual Studio Build Tools with “Desktop development with C++” workload
- Windows 10 or later
Building
perry app.ts -o app.exe --target windows
UI Toolkit
Perry maps UI widgets to Win32 controls:
| Perry Widget | Win32 Class |
|---|---|
| Text | Static HWND |
| Button | HWND Button |
| TextField | Edit HWND |
| SecureField | Edit (ES_PASSWORD) |
| Toggle | Checkbox |
| Slider | Trackbar (TRACKBAR_CLASSW) |
| Picker | ComboBox |
| ProgressView | PROGRESS_CLASSW |
| Image | GDI |
| VStack/HStack | Manual layout |
| ScrollView | WS_VSCROLL |
| Canvas | GDI drawing |
| Form/Section | GroupBox |
Windows-Specific APIs
- Menu bar: HMENU / SetMenu
- Dark mode: Windows Registry detection
- Preferences: Windows Registry
- Keychain: CredWrite/CredRead/CredDelete (Windows Credential Manager)
- Notifications: Toast notifications
- File dialogs: IFileOpenDialog / IFileSaveDialog (COM)
- Alerts: MessageBoxW
- Open URL: ShellExecuteW
Next Steps
- Platform Overview — All platforms
- UI Overview — UI system
Linux (GTK4)
Perry compiles TypeScript apps for Linux using GTK4.
Requirements
- GTK4 development libraries:
# Ubuntu/Debian sudo apt install libgtk-4-dev # Fedora sudo dnf install gtk4-devel # Arch sudo pacman -S gtk4 - Cairo development libraries (for canvas):
sudo apt install libcairo2-dev
Building
perry app.ts -o app --target linux
./app
UI Toolkit
Perry maps UI widgets to GTK4 widgets:
| Perry Widget | GTK4 Widget |
|---|---|
| Text | GtkLabel |
| Button | GtkButton |
| TextField | GtkEntry |
| SecureField | GtkPasswordEntry |
| Toggle | GtkSwitch |
| Slider | GtkScale |
| Picker | GtkDropDown |
| ProgressView | GtkProgressBar |
| Image | GtkImage |
| VStack | GtkBox (vertical) |
| HStack | GtkBox (horizontal) |
| ZStack | GtkOverlay |
| ScrollView | GtkScrolledWindow |
| Canvas | Cairo drawing |
| NavigationStack | GtkStack |
Linux-Specific APIs
- Menu bar: GMenu / set_menubar
- Toolbar: GtkHeaderBar
- Dark mode: GTK settings detection
- Preferences: GSettings or file-based
- Keychain: libsecret
- Notifications: GNotification
- File dialogs: GtkFileChooserDialog
- Alerts: GtkMessageDialog
Styling
GTK4 styling uses CSS under the hood. Perry’s styling methods (colors, fonts, corner radius) are translated to CSS properties applied via CssProvider.
Next Steps
- Platform Overview — All platforms
- UI Overview — UI system
Web
Perry can compile TypeScript UI apps to self-contained HTML files using --target web.
Building
perry app.ts -o app --target web
open app.html # Opens in your default browser
The output is a single .html file containing all JavaScript and CSS — no build step, no dependencies.
How It Works
Instead of using Cranelift for native code generation, the --target web flag uses the perry-codegen-js crate to emit JavaScript from HIR. The output is a self-contained HTML file with:
- Inline JavaScript (your compiled TypeScript)
- A web runtime that maps
perry/uiwidgets to DOM elements - CSS for layout (flexbox) and styling
The web target skips Cranelift, inlining, generator transforms, and closure conversion — JavaScript engines handle these natively.
UI Mapping
Perry widgets map to HTML elements:
| Perry Widget | HTML Element |
|---|---|
| Text | <span> |
| Button | <button> |
| TextField | <input type="text"> |
| SecureField | <input type="password"> |
| Toggle | <input type="checkbox"> |
| Slider | <input type="range"> |
| Picker | <select> |
| ProgressView | <progress> |
| Image | <img> |
| VStack | <div> (flexbox column) |
| HStack | <div> (flexbox row) |
| ZStack | <div> (position: relative/absolute) |
| ScrollView | <div> (overflow: auto) |
| Canvas | <canvas> (2D context) |
| Table | <table> |
Web-Specific Features
- Clipboard:
navigator.clipboardAPI - Notifications: Web Notification API
- Dark mode:
prefers-color-schememedia query - Keychain: localStorage (not truly secure — use for preferences only)
- Dialogs:
<input type="file">,alert(), modal<div> - Keyboard shortcuts: DOM keyboard event listeners
- Multi-window: Floating
<div>panels
Limitations
- No file system access (browser sandbox)
- No database connections
- No background processes
- localStorage instead of secure keychain
- Single-page — no native app lifecycle
Example
import { App, Text, Button, VStack, State } from "perry/ui";
const count = State(0);
App("Web Counter", () =>
VStack([
Text(`Count: ${count.get()}`),
Button("+1", () => count.set(count.get() + 1)),
])
);
perry counter.ts -o counter --target web
# Produces counter.html — open in any browser
Next Steps
- Platform Overview — All platforms
- UI Overview — UI system
Standard Library Overview
Perry natively implements many popular npm packages and Node.js APIs. When you import a supported package, Perry compiles it to native code — no JavaScript runtime involved.
How It Works
import fastify from "fastify";
import mysql from "mysql2/promise";
Perry recognizes these imports at compile time and routes them to native Rust implementations in the perry-stdlib crate. The API surface matches the original npm package, so existing code often works unchanged.
Supported Packages
Networking & HTTP
- fastify — HTTP server framework
- axios — HTTP client
- node-fetch / fetch — HTTP fetch API
- ws — WebSocket client/server
Databases
- mysql2 — MySQL client
- pg — PostgreSQL client
- better-sqlite3 — SQLite
- mongodb — MongoDB client
- ioredis / redis — Redis client
Cryptography
- bcrypt — Password hashing
- argon2 — Password hashing (Argon2)
- jsonwebtoken — JWT signing/verification
- crypto — Node.js crypto module
- ethers — Ethereum library
Utilities
- lodash — Utility functions
- dayjs / moment — Date manipulation
- uuid — UUID generation
- nanoid — ID generation
- slugify — String slugification
- validator — String validation
CLI & Data
- commander — CLI argument parsing
- decimal.js — Arbitrary precision decimals
- bignumber.js — Big number math
- lru-cache — LRU caching
Other
- sharp — Image processing
- cheerio — HTML parsing
- nodemailer — Email sending
- zlib — Compression
- cron — Job scheduling
- worker_threads — Background workers
- exponential-backoff — Retry logic
- async_hooks — AsyncLocalStorage
Node.js Built-ins
- fs — File system
- path — Path manipulation
- child_process — Process spawning
- crypto — Cryptographic functions
Binary Size
Perry automatically detects which stdlib features your code uses:
| Usage | Binary Size |
|---|---|
| No stdlib imports | ~300KB |
| fs + path only | ~3MB |
| Full stdlib | ~48MB |
The compiler links only the required runtime components.
compilePackages
For npm packages not natively supported, you can compile pure TypeScript/JavaScript packages natively:
{
"perry": {
"compilePackages": ["@noble/curves", "@noble/hashes"]
}
}
See Project Configuration for details.
JavaScript Runtime Fallback
For packages that can’t be compiled natively (native addons, dynamic code, etc.), Perry includes a QuickJS-based JavaScript runtime as a fallback:
import { jsEval } from "perry/jsruntime";
Next Steps
File System
Perry implements Node.js file system APIs for reading, writing, and managing files.
Reading Files
import { readFileSync } from "fs";
const content = readFileSync("config.json", "utf-8");
console.log(content);
Binary File Reading
import { readFileBuffer } from "fs";
const buffer = readFileBuffer("image.png");
console.log(`Read ${buffer.length} bytes`);
readFileBuffer reads files as binary data (uses fs::read() internally, not read_to_string()).
Writing Files
import { writeFileSync } from "fs";
writeFileSync("output.txt", "Hello, World!");
writeFileSync("data.json", JSON.stringify({ key: "value" }, null, 2));
File Information
import { existsSync, statSync } from "fs";
if (existsSync("config.json")) {
const stat = statSync("config.json");
console.log(`Size: ${stat.size}`);
}
Directory Operations
import { mkdirSync, readdirSync, rmRecursive } from "fs";
// Create directory
mkdirSync("output");
// Read directory contents
const files = readdirSync("src");
for (const file of files) {
console.log(file);
}
// Remove directory recursively
rmRecursive("output"); // Uses fs::remove_dir_all
Path Utilities
import { join, dirname, basename, resolve } from "path";
import { fileURLToPath } from "url";
const dir = dirname(fileURLToPath(import.meta.url));
const configPath = join(dir, "config.json");
const name = basename(configPath); // "config.json"
const abs = resolve("relative/path"); // Absolute path
Next Steps
- HTTP & Networking
- Overview — All stdlib modules
HTTP & Networking
Perry natively implements HTTP servers, clients, and WebSocket support.
Fastify Server
import fastify from "fastify";
const app = fastify();
app.get("/", async (request, reply) => {
return { hello: "world" };
});
app.get("/users/:id", async (request, reply) => {
const { id } = request.params;
return { id, name: "User " + id };
});
app.post("/data", async (request, reply) => {
const body = request.body;
reply.code(201);
return { received: body };
});
app.listen({ port: 3000 }, () => {
console.log("Server running on port 3000");
});
Perry’s Fastify implementation is API-compatible with the npm package. Routes, request/reply objects, params, query strings, and JSON body parsing all work.
Fetch API
// GET request
const response = await fetch("https://api.example.com/data");
const data = await response.json();
// POST request
const result = await fetch("https://api.example.com/data", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ key: "value" }),
});
Axios
import axios from "axios";
const { data } = await axios.get("https://api.example.com/users");
const response = await axios.post("https://api.example.com/users", {
name: "Perry",
email: "perry@example.com",
});
WebSocket
import { WebSocket } from "ws";
const ws = new WebSocket("ws://localhost:8080");
ws.on("open", () => {
ws.send("Hello, server!");
});
ws.on("message", (data) => {
console.log(`Received: ${data}`);
});
ws.on("close", () => {
console.log("Connection closed");
});
Next Steps
Databases
Perry natively implements clients for MySQL, PostgreSQL, SQLite, MongoDB, and Redis.
MySQL
import mysql from "mysql2/promise";
const connection = await mysql.createConnection({
host: "localhost",
user: "root",
password: "password",
database: "mydb",
});
const [rows] = await connection.execute("SELECT * FROM users WHERE id = ?", [1]);
console.log(rows);
await connection.end();
PostgreSQL
import { Client } from "pg";
const client = new Client({
host: "localhost",
port: 5432,
user: "postgres",
password: "password",
database: "mydb",
});
await client.connect();
const result = await client.query("SELECT * FROM users WHERE id = $1", [1]);
console.log(result.rows);
await client.end();
SQLite
import Database from "better-sqlite3";
const db = new Database("mydb.sqlite");
db.exec(`
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT,
email TEXT
)
`);
const insert = db.prepare("INSERT INTO users (name, email) VALUES (?, ?)");
insert.run("Perry", "perry@example.com");
const users = db.prepare("SELECT * FROM users").all();
console.log(users);
MongoDB
import { MongoClient } from "mongodb";
const client = new MongoClient("mongodb://localhost:27017");
await client.connect();
const db = client.db("mydb");
const users = db.collection("users");
await users.insertOne({ name: "Perry", email: "perry@example.com" });
const user = await users.findOne({ name: "Perry" });
console.log(user);
await client.close();
Redis
import Redis from "ioredis";
const redis = new Redis();
await redis.set("key", "value");
const value = await redis.get("key");
console.log(value); // "value"
await redis.del("key");
await redis.quit();
Next Steps
- Cryptography
- Overview — All stdlib modules
Cryptography
Perry natively implements password hashing, JWT tokens, and Ethereum cryptography.
bcrypt
import bcrypt from "bcrypt";
const hash = await bcrypt.hash("mypassword", 10);
const match = await bcrypt.compare("mypassword", hash);
console.log(match); // true
Argon2
import argon2 from "argon2";
const hash = await argon2.hash("mypassword");
const valid = await argon2.verify(hash, "mypassword");
console.log(valid); // true
JSON Web Tokens
import jwt from "jsonwebtoken";
const secret = "my-secret-key";
// Sign a token
const token = jwt.sign({ userId: 123, role: "admin" }, secret, {
expiresIn: "1h",
});
// Verify a token
const decoded = jwt.verify(token, secret);
console.log(decoded.userId); // 123
Node.js Crypto
import crypto from "crypto";
// Hash
const hash = crypto.createHash("sha256").update("data").digest("hex");
// HMAC
const hmac = crypto.createHmac("sha256", "secret").update("data").digest("hex");
// Random bytes
const bytes = crypto.randomBytes(32);
Ethers
import { ethers } from "ethers";
// Create a wallet
const wallet = ethers.Wallet.createRandom();
console.log(wallet.address);
// Sign a message
const signature = await wallet.signMessage("Hello, Ethereum!");
Next Steps
Utilities
Perry natively implements common utility packages.
lodash
import _ from "lodash";
_.chunk([1, 2, 3, 4, 5], 2); // [[1,2], [3,4], [5]]
_.uniq([1, 2, 2, 3, 3]); // [1, 2, 3]
_.groupBy(users, "role");
_.sortBy(users, ["name"]);
_.cloneDeep(obj);
_.merge(defaults, overrides);
_.debounce(fn, 300);
_.throttle(fn, 100);
dayjs
import dayjs from "dayjs";
const now = dayjs();
console.log(now.format("YYYY-MM-DD"));
console.log(now.add(7, "day").format("YYYY-MM-DD"));
console.log(now.subtract(1, "month").toISOString());
const diff = dayjs("2025-12-31").diff(now, "day");
console.log(`${diff} days until end of year`);
moment
import moment from "moment";
const now = moment();
console.log(now.format("MMMM Do YYYY"));
console.log(now.fromNow());
console.log(moment("2025-01-01").isBefore(now));
uuid
import { v4 as uuidv4 } from "uuid";
const id = uuidv4();
console.log(id); // e.g., "550e8400-e29b-41d4-a716-446655440000"
nanoid
import { nanoid } from "nanoid";
const id = nanoid(); // Default 21 chars
const short = nanoid(10); // Custom length
console.log(id);
slugify
import slugify from "slugify";
const slug = slugify("Hello World!", { lower: true });
console.log(slug); // "hello-world"
validator
import validator from "validator";
validator.isEmail("test@example.com"); // true
validator.isURL("https://example.com"); // true
validator.isUUID(id); // true
validator.isEmpty(""); // true
Next Steps
- Other Modules
- Overview — All stdlib modules
Other Modules
Additional npm packages and Node.js APIs supported by Perry.
sharp (Image Processing)
import sharp from "sharp";
await sharp("input.jpg")
.resize(300, 200)
.toFile("output.png");
cheerio (HTML Parsing)
import cheerio from "cheerio";
const html = "<html><body><h1>Hello</h1><p>World</p></body></html>";
const $ = cheerio.load(html);
console.log($("h1").text()); // "Hello"
nodemailer (Email)
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: "smtp.example.com",
port: 587,
auth: { user: "user", pass: "pass" },
});
await transporter.sendMail({
from: "sender@example.com",
to: "recipient@example.com",
subject: "Hello from Perry",
text: "This email was sent from a compiled TypeScript binary!",
});
zlib (Compression)
import zlib from "zlib";
const compressed = zlib.gzipSync("Hello, World!");
const decompressed = zlib.gunzipSync(compressed);
cron (Job Scheduling)
import { CronJob } from "cron";
const job = new CronJob("*/5 * * * *", () => {
console.log("Runs every 5 minutes");
});
job.start();
worker_threads
import { Worker, parentPort, workerData } from "worker_threads";
if (parentPort) {
// Worker thread
const data = workerData;
parentPort.postMessage({ result: data.value * 2 });
} else {
// Main thread
const worker = new Worker("./worker.ts", {
workerData: { value: 21 },
});
worker.on("message", (msg) => {
console.log(msg.result); // 42
});
}
commander (CLI Parsing)
import { Command } from "commander";
const program = new Command();
program.name("my-cli").version("1.0.0").description("My CLI tool");
program
.command("serve")
.option("-p, --port <number>", "Port number")
.option("--verbose", "Verbose output")
.action((options) => {
console.log(`Starting server on port ${options.port}`);
});
program.parse(process.argv);
decimal.js (Arbitrary Precision)
import Decimal from "decimal.js";
const a = new Decimal("0.1");
const b = new Decimal("0.2");
const sum = a.plus(b); // Exactly 0.3 (no floating point errors)
sum.toFixed(2); // "0.30"
sum.toNumber(); // 0.3
a.times(b); // 0.02
a.div(b); // 0.5
a.pow(10); // 1e-10
a.sqrt(); // 0.316...
lru-cache
import LRUCache from "lru-cache";
const cache = new LRUCache(100); // max 100 entries
cache.set("key", "value");
cache.get("key"); // "value"
cache.has("key"); // true
cache.delete("key");
cache.clear();
child_process
import { spawnBackground, getProcessStatus, killProcess } from "child_process";
// Spawn a background process
const { pid, handleId } = spawnBackground("sleep", ["10"], "/tmp/log.txt");
// Check if it's still running
const status = getProcessStatus(handleId);
console.log(status.alive); // true
// Kill it
killProcess(handleId);
Next Steps
- Overview — All stdlib modules
- File System — fs and path APIs
System APIs Overview
The perry/system module provides access to platform-native system features: preferences, secure storage, notifications, URL opening, and dark mode detection.
import { openURL, isDarkMode, preferencesSet, preferencesGet } from "perry/system";
Available APIs
| Function | Description | Platforms |
|---|---|---|
openURL(url) | Open URL in default browser/app | All |
isDarkMode() | Check system dark mode | All |
preferencesSet(key, value) | Store a preference | All |
preferencesGet(key) | Read a preference | All |
keychainSet(key, value) | Secure storage write | All |
keychainGet(key) | Secure storage read | All |
sendNotification(title, body) | Local notification | All |
clipboardGet() | Read clipboard | All |
clipboardSet(text) | Write clipboard | All |
Quick Example
import { isDarkMode, preferencesGet, preferencesSet, openURL } from "perry/system";
// Detect dark mode
if (isDarkMode()) {
console.log("Dark mode is active");
}
// Store user preferences
preferencesSet("theme", "dark");
const theme = preferencesGet("theme");
// Open a URL
openURL("https://example.com");
Next Steps
Preferences
Store and retrieve user preferences using the platform’s native storage.
Usage
import { preferencesSet, preferencesGet } from "perry/system";
// Store a preference
preferencesSet("username", "perry");
preferencesSet("fontSize", "14");
preferencesSet("darkMode", "true");
// Read a preference
const username = preferencesGet("username"); // "perry"
const fontSize = preferencesGet("fontSize"); // "14"
Values are stored as strings. Convert numbers and booleans as needed:
preferencesSet("count", String(42));
const count = Number(preferencesGet("count"));
Platform Storage
| Platform | Backend |
|---|---|
| macOS | NSUserDefaults |
| iOS | NSUserDefaults |
| Android | SharedPreferences |
| Windows | Windows Registry |
| Linux | GSettings / file-based |
| Web | localStorage |
Preferences persist across app launches. They are not encrypted — use Keychain for sensitive data.
Next Steps
Keychain
Securely store sensitive data like tokens, passwords, and API keys using the platform’s secure storage.
Usage
import { keychainSet, keychainGet } from "perry/system";
// Store a secret
keychainSet("api-token", "sk-abc123...");
// Retrieve a secret
const token = keychainGet("api-token");
Platform Storage
| Platform | Backend |
|---|---|
| macOS | Security.framework (Keychain) |
| iOS | Security.framework (Keychain) |
| Android | Android Keystore |
| Windows | Windows Credential Manager (CredWrite/CredRead/CredDelete) |
| Linux | libsecret |
| Web | localStorage (not truly secure) |
Web: The web platform uses
localStorage, which is not encrypted. For web apps handling sensitive data, consider server-side storage instead.
Next Steps
- Preferences — Non-sensitive preferences
- Notifications — Local notifications
- Overview — All system APIs
Notifications
Send local notifications using the platform’s notification system.
Usage
import { sendNotification } from "perry/system";
sendNotification("Download Complete", "Your file has been downloaded successfully.");
Platform Implementation
| Platform | Backend |
|---|---|
| macOS | UNUserNotificationCenter |
| iOS | UNUserNotificationCenter |
| Android | NotificationManager |
| Windows | Toast notifications |
| Linux | GNotification |
| Web | Web Notification API |
Permissions: On macOS, iOS, and Web, the user may need to grant notification permissions. On first use, the system will prompt for permission automatically.
Next Steps
Other System APIs
Additional platform-level APIs.
Open URL
Open a URL in the default browser or application:
import { openURL } from "perry/system";
openURL("https://example.com");
openURL("mailto:user@example.com");
| Platform | Implementation |
|---|---|
| macOS | NSWorkspace.open |
| iOS | UIApplication.open |
| Android | Intent.ACTION_VIEW |
| Windows | ShellExecuteW |
| Linux | xdg-open |
| Web | window.open |
Dark Mode Detection
import { isDarkMode } from "perry/system";
if (isDarkMode()) {
// Use dark theme colors
}
| Platform | Detection |
|---|---|
| macOS | NSApp.effectiveAppearance |
| iOS | UITraitCollection |
| Android | Configuration.uiMode |
| Windows | Registry (AppsUseLightTheme) |
| Linux | GTK settings |
| Web | prefers-color-scheme media query |
Clipboard
import { clipboardGet, clipboardSet } from "perry/system";
clipboardSet("Copied text!");
const text = clipboardGet();
Next Steps
- Overview — All system APIs
- UI Overview — Building UIs
Widgets (WidgetKit) Overview
Perry can compile TypeScript widget declarations to native SwiftUI WidgetKit extensions for iOS home screen widgets.
What Are Widgets?
iOS home screen widgets display glanceable information outside your app. Perry’s perry/widget module lets you define widgets in TypeScript that compile to native SwiftUI code.
import { Widget, Text, VStack } from "perry/widget";
Widget({
kind: "MyWidget",
displayName: "My Widget",
description: "Shows a greeting",
entryFields: { name: "string" },
render: (entry) =>
VStack([
Text(`Hello, ${entry.name}!`),
]),
});
How It Works
TypeScript widget declaration
↓ Parse & Lower to WidgetDecl HIR
↓ perry-codegen-swiftui emits SwiftUI source
↓
Complete WidgetKit extension:
- Entry struct
- View
- TimelineProvider
- WidgetBundle
- Info.plist
The compiler generates a complete SwiftUI WidgetKit extension — no Swift knowledge required.
Building
perry widget.ts --target ios-widget
This produces a WidgetKit extension directory that can be added to an Xcode project.
Next Steps
- Creating Widgets — Widget() API in detail
- Components — Available widget components
- iOS Platform — iOS cross-compilation
Creating Widgets
Define iOS home screen widgets using the Widget() function.
Widget Declaration
import { Widget, Text, VStack, HStack, Image, Spacer } from "perry/widget";
Widget({
kind: "WeatherWidget",
displayName: "Weather",
description: "Shows current weather",
entryFields: {
temperature: "number",
condition: "string",
location: "string",
},
render: (entry) =>
VStack([
HStack([
Text(entry.location),
Spacer(),
Image("cloud.sun.fill"),
]),
Text(`${entry.temperature}°`),
Text(entry.condition),
]),
});
Widget Options
| Property | Type | Description |
|---|---|---|
kind | string | Unique identifier for the widget |
displayName | string | Name shown in widget gallery |
description | string | Description in widget gallery |
entryFields | object | Data fields with types ("string", "number", "boolean") |
render | function | Render function receiving entry data, returns widget tree |
Entry Fields
Entry fields define the data your widget displays. Each field has a name and type:
entryFields: {
title: "string",
count: "number",
isActive: "boolean",
}
These compile to a Swift TimelineEntry struct:
struct WeatherEntry: TimelineEntry {
let date: Date
let temperature: Double
let condition: String
let location: String
}
Conditionals in Render
Use ternary expressions for conditional rendering:
render: (entry) =>
VStack([
Text(entry.isActive ? "Active" : "Inactive"),
entry.count > 0 ? Text(`${entry.count} items`) : Spacer(),
]),
Template Literals
Template literals in widget text are compiled to Swift string interpolation:
Text(`${entry.name}: ${entry.score} points`)
// Compiles to: Text("\(entry.name): \(entry.score) points")
Multiple Widgets
Define multiple widgets in a single file. They’re bundled into a WidgetBundle:
Widget({
kind: "SmallWidget",
// ...
});
Widget({
kind: "LargeWidget",
// ...
});
Next Steps
- Components — Available widget components and modifiers
- Overview — Widget system overview
Widget Components
Available components and modifiers for WidgetKit widgets.
Text
Text("Hello, World!")
Text(`${entry.name}: ${entry.value}`)
Text Modifiers
const t = Text("Styled");
t.font("title"); // .title, .headline, .body, .caption, etc.
t.color("blue"); // Named color or hex
t.bold();
Layout
VStack
VStack([
Text("Top"),
Text("Bottom"),
])
HStack
HStack([
Text("Left"),
Spacer(),
Text("Right"),
])
ZStack
ZStack([
Image("background"),
Text("Overlay"),
])
Spacer
Flexible space that expands to fill available room:
HStack([
Text("Left"),
Spacer(),
Text("Right"),
])
Image
Display SF Symbols or asset images:
Image("star.fill") // SF Symbol
Image("cloud.sun.rain.fill") // SF Symbol
Modifiers
Widget components support SwiftUI-style modifiers:
Font
Text("Title").font("title")
Text("Body").font("body")
Text("Caption").font("caption")
Color
Text("Red text").color("red")
Text("Custom").color("#FF6600")
Padding
VStack([...]).padding(16)
Frame
widget.frame(width, height)
Conditionals
Render different components based on entry data:
render: (entry) =>
VStack([
entry.isOnline
? Text("Online").color("green")
: Text("Offline").color("red"),
]),
Complete Example
import { Widget, Text, VStack, HStack, Image, Spacer } from "perry/widget";
Widget({
kind: "StatsWidget",
displayName: "Stats",
description: "Shows daily stats",
entryFields: {
steps: "number",
calories: "number",
distance: "string",
},
render: (entry) =>
VStack([
HStack([
Image("figure.walk"),
Text("Daily Stats").font("headline"),
]),
Spacer(),
HStack([
VStack([
Text(`${entry.steps}`).font("title").bold(),
Text("steps").font("caption").color("gray"),
]),
Spacer(),
VStack([
Text(`${entry.calories}`).font("title").bold(),
Text("cal").font("caption").color("gray"),
]),
Spacer(),
VStack([
Text(entry.distance).font("title").bold(),
Text("km").font("caption").color("gray"),
]),
]),
]).padding(16),
});
Next Steps
- Creating Widgets — Widget() API
- Overview — Widget system overview
Plugin System Overview
Perry supports native plugins as shared libraries (.dylib/.so). Plugins extend Perry applications with custom hooks, tools, services, and routes.
How It Works
- A plugin is a Perry-compiled shared library with
activate(api)anddeactivate()entry points - The host application loads plugins with
loadPlugin(path) - Plugins register hooks, tools, and services via the API handle
- The host dispatches events to plugins via
emitHook(name, data)
Host Application
↓ loadPlugin("./my-plugin.dylib")
↓ calls plugin_activate(api_handle)
Plugin
↓ api.registerHook("beforeSave", callback)
↓ api.registerTool("format", callback)
Host
↓ emitHook("beforeSave", data) → plugin callback runs
Quick Example
Plugin (compiled with --output-type dylib)
// my-plugin.ts
export function activate(api: PluginAPI) {
api.setMetadata("my-plugin", "1.0.0", "A sample plugin");
api.registerHook("beforeSave", (data) => {
console.log("About to save:", data);
return data; // Return modified data (filter mode)
});
api.registerTool("greet", (args) => {
return `Hello, ${args.name}!`;
});
}
export function deactivate() {
console.log("Plugin deactivated");
}
perry my-plugin.ts --output-type dylib -o my-plugin.dylib
Host Application
import { loadPlugin, emitHook, invokeTool, listPlugins } from "perry/plugin";
loadPlugin("./my-plugin.dylib");
// List loaded plugins
const plugins = listPlugins();
console.log(plugins); // [{ name: "my-plugin", version: "1.0.0", ... }]
// Emit a hook
const result = emitHook("beforeSave", { content: "..." });
// Invoke a tool
const greeting = invokeTool("greet", { name: "Perry" });
console.log(greeting); // "Hello, Perry!"
Plugin ABI
Plugins must export these symbols:
perry_plugin_abi_version()— Returns ABI version (for compatibility checking)plugin_activate(api_handle)— Called when plugin is loadedplugin_deactivate()— Called when plugin is unloaded
Perry generates these automatically from your activate/deactivate exports.
Next Steps
- Creating Plugins — Build a plugin step by step
- Hooks & Events — Hook modes, event bus, tools
Creating Plugins
Build Perry plugins as shared libraries that extend host applications.
Step 1: Write the Plugin
// counter-plugin.ts
let count = 0;
export function activate(api: PluginAPI) {
api.setMetadata("counter", "1.0.0", "Counts hook invocations");
api.registerHook("onRequest", (data) => {
count++;
console.log(`Request #${count}`);
return data;
});
api.registerTool("getCount", () => {
return count;
});
}
export function deactivate() {
console.log(`Total requests processed: ${count}`);
}
Step 2: Compile as Shared Library
perry counter-plugin.ts --output-type dylib -o counter-plugin.dylib
The --output-type dylib flag tells Perry to produce a .dylib (macOS) or .so (Linux) instead of an executable.
Perry automatically:
- Generates
perry_plugin_abi_version()returning the current ABI version - Generates
plugin_activate(api_handle)calling youractivate()function - Generates
plugin_deactivate()calling yourdeactivate()function - Exports symbols with
-rdynamicfor the host to find
Step 3: Load from Host
// host-app.ts
import { loadPlugin, emitHook, invokeTool, discoverPlugins } from "perry/plugin";
// Load a specific plugin
loadPlugin("./counter-plugin.dylib");
// Or discover plugins in a directory
discoverPlugins("./plugins/");
// Use the plugin
emitHook("onRequest", { path: "/api/users" });
const count = invokeTool("getCount", {});
console.log(`Processed ${count} requests`);
Plugin API Reference
The api object passed to activate() provides:
Metadata
api.setMetadata(name: string, version: string, description: string)
Hooks
api.registerHook(name: string, callback: (data: any) => any, priority?: number)
Hooks are called in priority order (lower number = called first).
Tools
api.registerTool(name: string, callback: (args: any) => any)
Tools are invoked by name from the host.
Configuration
const value = api.getConfig(key: string) // Read host-provided config
Events
api.on(event: string, handler: (data: any) => void) // Listen for events
api.emit(event: string, data: any) // Emit to other plugins
Next Steps
- Hooks & Events — Hook modes, event bus
- Overview — Plugin system overview
Hooks & Events
Perry plugins communicate through hooks, events, and tools.
Hook Modes
Hooks support three execution modes:
Filter Mode (default)
Each plugin receives data and returns (possibly modified) data. The output of one plugin becomes the input of the next:
api.registerHook("transform", (data) => {
data.content = data.content.toUpperCase();
return data; // Returned data goes to next plugin
});
Action Mode
Plugins receive data but return value is ignored. Used for side effects:
api.registerHook("onSave", (data) => {
console.log(`Saved: ${data.path}`);
// Return value ignored
});
Waterfall Mode
Like filter mode, but specifically for accumulating/building up a result through the chain:
api.registerHook("buildMenu", (items) => {
items.push({ label: "My Plugin Action", action: () => {} });
return items;
});
Hook Priority
Lower priority numbers run first:
api.registerHook("beforeSave", validate, 10); // Runs first
api.registerHook("beforeSave", transform, 20); // Runs second
api.registerHook("beforeSave", log, 100); // Runs last
Default priority is 50.
Event Bus
Plugins can communicate with each other through events:
Emitting Events
// From a plugin
api.emit("dataUpdated", { source: "my-plugin", records: 42 });
// From the host
import { emitEvent } from "perry/plugin";
emitEvent("dataUpdated", { source: "host", records: 100 });
Listening for Events
api.on("dataUpdated", (data) => {
console.log(`${data.source} updated ${data.records} records`);
});
Tools
Plugins register callable tools:
// Plugin registers a tool
api.registerTool("formatCode", (args) => {
return formatSource(args.code, args.language);
});
// Host invokes the tool
import { invokeTool } from "perry/plugin";
const formatted = invokeTool("formatCode", {
code: "const x=1",
language: "typescript",
});
Configuration
Hosts can pass configuration to plugins:
// Host sets config
import { setConfig } from "perry/plugin";
setConfig("theme", "dark");
setConfig("maxRetries", "3");
// Plugin reads config
export function activate(api: PluginAPI) {
const theme = api.getConfig("theme"); // "dark"
const retries = api.getConfig("maxRetries"); // "3"
}
Introspection
Query loaded plugins and their registrations:
import { listPlugins, listHooks, listTools } from "perry/plugin";
const plugins = listPlugins(); // [{ name, version, description }]
const hooks = listHooks(); // [{ name, pluginName, priority }]
const tools = listTools(); // [{ name, pluginName }]
Next Steps
- Creating Plugins — Build a plugin
- Overview — Plugin system overview
CLI Commands
Perry provides 8 commands for compiling, checking, publishing, and managing your projects.
compile
Compile TypeScript to a native executable.
perry compile main.ts -o app
# Or shorthand (auto-detects compile):
perry main.ts -o app
| Flag | Description |
|---|---|
-o, --output <PATH> | Output file path |
--target <TARGET> | Platform target (see Compiler Flags) |
--output-type <TYPE> | executable (default) or dylib (plugin) |
--print-hir | Print HIR intermediate representation |
--no-link | Produce object file only, skip linking |
--keep-intermediates | Keep .o and .asm files |
--enable-js-runtime | Enable V8 JavaScript runtime fallback |
--type-check | Enable type checking via tsgo |
--app-bundle-id <ID> | Bundle ID (required for widget targets) |
--bundle-extensions <DIR> | Bundle TypeScript extensions from directory |
# Basic compilation
perry compile app.ts -o app
# Cross-compile for iOS Simulator
perry compile app.ts -o app --target ios-simulator
# Build a plugin
perry compile plugin.ts --output-type dylib -o plugin.dylib
# Debug: view intermediate representation
perry compile app.ts --print-hir
# Build an iOS widget
perry compile widget.ts --target ios-widget --app-bundle-id com.myapp.widget
check
Validate TypeScript for Perry compatibility without compiling.
perry check src/
| Flag | Description |
|---|---|
--check-deps | Check node_modules for compatibility |
--deep-deps | Scan all transitive dependencies |
--all | Show all issues including hints |
--strict | Treat warnings as errors |
--fix | Automatically apply fixes |
--fix-dry-run | Preview fixes without modifying files |
--fix-unsafe | Include medium-confidence fixes |
# Check a single file
perry check src/index.ts
# Check with dependency analysis
perry check . --check-deps
# Auto-fix issues
perry check . --fix
# Preview fixes without applying
perry check . --fix-dry-run
init
Create a new Perry project.
perry init my-project
cd my-project
| Flag | Description |
|---|---|
--name <NAME> | Project name (defaults to directory name) |
Creates perry.toml, src/main.ts, and .gitignore.
doctor
Check your Perry installation and environment.
perry doctor
| Flag | Description |
|---|---|
--quiet | Only report failures |
Checks:
- Perry version
- System linker availability (cc/MSVC)
- Runtime library
- Project configuration
- Available updates
explain
Get detailed explanations for error codes.
perry explain U001
Error code families:
- P — Parse errors
- T — Type errors
- U — Unsupported features
- D — Dependency issues
Each explanation includes the error description, example code, and suggested fix.
publish
Build, sign, and distribute your app.
perry publish --macos
perry publish --ios
perry publish --android
| Flag | Description |
|---|---|
--macos | Build for macOS (App Store/notarization) |
--ios | Build for iOS (App Store/TestFlight) |
--android | Build for Android (Google Play) |
--linux | Build for Linux (AppImage/deb/rpm) |
--server <URL> | Build server (default: https://hub.perryts.com) |
--license-key <KEY> | Perry Hub license key |
--project <PATH> | Project directory |
-o, --output <PATH> | Artifact output directory (default: dist) |
--no-download | Skip artifact download |
Apple-specific flags:
| Flag | Description |
|---|---|
--apple-team-id <ID> | Developer Team ID |
--apple-identity <NAME> | Signing identity |
--apple-p8-key <PATH> | App Store Connect .p8 key |
--apple-key-id <ID> | App Store Connect API Key ID |
--apple-issuer-id <ID> | App Store Connect Issuer ID |
--certificate <PATH> | .p12 certificate bundle |
--provisioning-profile <PATH> | .mobileprovision file (iOS) |
Android-specific flags:
| Flag | Description |
|---|---|
--android-keystore <PATH> | .jks/.keystore file |
--android-keystore-password <PASS> | Keystore password |
--android-key-alias <ALIAS> | Key alias |
--android-key-password <PASS> | Key password |
--google-play-key <PATH> | Google Play service account JSON |
On first use, publish auto-registers a free license key.
setup
Interactive credential wizard for app distribution.
perry setup # Show platform menu
perry setup macos # macOS setup
perry setup ios # iOS setup
perry setup android # Android setup
Stores credentials in ~/.perry/config.toml.
update
Check for and install Perry updates.
perry update # Update to latest
perry update --check-only # Check without installing
perry update --force # Ignore 24h cache
Update sources (checked in order):
- Custom server (env/config)
- Perry Hub
- GitHub API
Opt out of automatic update checks with PERRY_NO_UPDATE_CHECK=1 or CI=true.
Next Steps
- Compiler Flags — Complete flag reference
- Getting Started — Installation
Compiler Flags
Complete reference for all Perry CLI flags.
Global Flags
Available on all commands:
| Flag | Description |
|---|---|
--format text|json | Output format (default: text) |
-v, --verbose | Increase verbosity (repeatable: -v, -vv, -vvv) |
-q, --quiet | Suppress non-error output |
--no-color | Disable ANSI color codes |
Compilation Targets
Use --target to cross-compile:
| Target | Platform | Notes |
|---|---|---|
| (none) | Current platform | Default behavior |
ios-simulator | iOS Simulator | ARM64 simulator binary |
ios | iOS Device | ARM64 device binary |
android | Android | ARM64/ARMv7 |
ios-widget | iOS Widget | WidgetKit extension (requires --app-bundle-id) |
ios-widget-simulator | iOS Widget (Sim) | Widget for simulator |
web | Web | Outputs HTML file with JS |
windows | Windows | Win32 executable |
linux | Linux | GTK4 executable |
Output Types
Use --output-type to change what’s produced:
| Type | Description |
|---|---|
executable | Standalone binary (default) |
dylib | Shared library (.dylib/.so) for plugins |
Debug Flags
| Flag | Description |
|---|---|
--print-hir | Print HIR (intermediate representation) to stdout |
--no-link | Produce .o object file only, skip linking |
--keep-intermediates | Keep .o and .asm intermediate files |
Runtime Flags
| Flag | Description |
|---|---|
--enable-js-runtime | Enable V8 JavaScript runtime for unsupported npm packages |
--type-check | Enable type checking via tsgo IPC |
Environment Variables
| Variable | Description |
|---|---|
PERRY_LICENSE_KEY | Perry Hub license key for perry publish |
PERRY_APPLE_CERTIFICATE_PASSWORD | Password for .p12 certificate |
PERRY_NO_UPDATE_CHECK=1 | Disable automatic update checks |
PERRY_UPDATE_SERVER | Custom update server URL |
CI=true | Auto-skip update checks (set by most CI systems) |
RUST_LOG | Debug logging level (debug, info, trace) |
Configuration Files
perry.toml (project)
[project]
name = "my-app"
entry = "src/main.ts"
version = "1.0.0"
[build]
out_dir = "build"
[app]
name = "My App"
description = "A Perry application"
[macos]
bundle_id = "com.example.myapp"
category = "public.app-category.developer-tools"
minimum_os = "13.0"
distribute = "notarize" # "appstore", "notarize", or "both"
[ios]
bundle_id = "com.example.myapp"
deployment_target = "16.0"
device_family = ["iphone", "ipad"]
[android]
package_name = "com.example.myapp"
min_sdk = 26
target_sdk = 34
[linux]
format = "appimage" # "appimage", "deb", "rpm"
category = "Development"
~/.perry/config.toml (global)
[apple]
team_id = "XXXXXXXXXX"
signing_identity = "Developer ID Application: Your Name"
[android]
keystore_path = "/path/to/keystore.jks"
key_alias = "my-key"
Examples
# Simple CLI program
perry main.ts -o app
# iOS app for simulator
perry app.ts -o app --target ios-simulator
# Web app
perry app.ts -o app --target web
# Plugin shared library
perry plugin.ts --output-type dylib -o plugin.dylib
# iOS widget with bundle ID
perry widget.ts --target ios-widget --app-bundle-id com.example.app
# Debug compilation
perry app.ts --print-hir 2>&1 | less
# Verbose compilation
perry compile app.ts -o app -vvv
# Type-checked compilation
perry app.ts -o app --type-check
Next Steps
- Commands — All CLI commands
- Platform Overview — Platform targets
Architecture
This is a brief overview for contributors. For detailed implementation notes, see the project’s CLAUDE.md.
Compilation Pipeline
TypeScript (.ts)
↓ Parse (SWC)
↓ AST
↓ Lower (perry-hir)
↓ HIR (High-level IR)
↓ Transform (inline, closure conversion, async lowering)
↓ Codegen (Cranelift)
↓ Object file (.o)
↓ Link (system cc)
↓
Native Executable
Crate Map
| Crate | Purpose |
|---|---|
perry | CLI driver, command parsing, compilation orchestration |
perry-parser | SWC wrapper for TypeScript parsing |
perry-types | Type system definitions |
perry-hir | HIR data structures (ir.rs) and AST→HIR lowering (lower.rs) |
perry-transform | IR passes: function inlining, closure conversion, async lowering |
perry-codegen | Cranelift-based native code generation (12 modules) |
perry-codegen-js | JavaScript code generation for --target web |
perry-codegen-swiftui | SwiftUI code generation for WidgetKit extensions |
perry-runtime | Runtime library: NaN-boxed values, GC, arena allocator, objects, arrays, strings |
perry-stdlib | Node.js API implementations: mysql2, redis, fastify, bcrypt, etc. |
perry-ui | Shared UI types |
perry-ui-macos | macOS UI (AppKit) |
perry-ui-ios | iOS UI (UIKit) |
perry-jsruntime | JavaScript interop via QuickJS |
Key Concepts
NaN-Boxing
All JavaScript values are represented as 64-bit NaN-boxed values. The upper 16 bits encode the type tag:
| Tag | Type |
|---|---|
0x7FFF | String (lower 48 bits = pointer) |
0x7FFD | Pointer/Object (lower 48 bits = pointer) |
0x7FFE | Int32 (lower 32 bits = integer) |
0x7FFA | BigInt (lower 48 bits = pointer) |
| Special constants | undefined, null, true, false |
| Any other | Float64 (the full 64 bits) |
Garbage Collection
Mark-sweep GC with conservative stack scanning. Arena-allocated objects (arrays, objects) are found by linear block walking. Malloc-allocated objects (strings, closures, promises) are tracked in a thread-local Vec.
Handle-Based UI
UI widgets are represented as small integer handles NaN-boxed with POINTER_TAG. Each handle maps to a native platform widget (NSButton, UILabel, GtkButton, etc.). Two dispatch tables route method calls and property accesses to the correct FFI function.
Source Code Organization
The codegen crate was split into 12 focused modules:
perry-codegen/src/
codegen.rs # Main entry, module compilation
types.rs # Type definitions, context structs
util.rs # Helper functions
stubs.rs # Stub generation for unresolved deps
runtime_decls.rs # Runtime function declarations
classes.rs # Class compilation
functions.rs # Function compilation
closures.rs # Closure compilation
module_init.rs # Module initialization
stmt.rs # Statement compilation
expr.rs # Expression compilation
The HIR lowering was split into 8 modules:
perry-hir/src/
lower.rs # Main lowering entry
analysis.rs # Code analysis passes
enums.rs # Enum lowering
jsx.rs # JSX lowering
lower_types.rs # Type lowering
lower_patterns.rs # Pattern lowering
destructuring.rs # Destructuring lowering
lower_decl.rs # Declaration lowering
Next Steps
- Building from Source
- See
CLAUDE.mdin the repository root for detailed implementation notes
Building from Source
Prerequisites
- Rust toolchain (stable): rustup.rs
- System C compiler (
ccon macOS/Linux, MSVC on Windows)
Build
git clone https://github.com/skelpo/perry.git
cd perry
# Build all crates (release mode recommended)
cargo build --release
The binary is at target/release/perry.
Build Specific Crates
# Runtime only (must rebuild stdlib too!)
cargo build --release -p perry-runtime -p perry-stdlib
# Codegen only
cargo build --release -p perry-codegen
Important: When rebuilding
perry-runtime, you must also rebuildperry-stdlibbecauselibperry_stdlib.aembeds perry-runtime as a static dependency.
Run Tests
# All tests (exclude iOS crate on non-iOS host)
cargo test --workspace --exclude perry-ui-ios
# Specific crate
cargo test -p perry-hir
cargo test -p perry-codegen
Compile and Run TypeScript
# Compile a TypeScript file
cargo run --release -- hello.ts -o hello
./hello
# Debug: print HIR
cargo run --release -- hello.ts --print-hir
Development Workflow
- Make changes to the relevant crate
cargo build --releaseto buildcargo test --workspace --exclude perry-ui-iosto verify- Test with a real TypeScript file:
cargo run --release -- test.ts -o test && ./test
Project Structure
perry/
├── crates/
│ ├── perry/ # CLI driver
│ ├── perry-parser/ # SWC TypeScript parser
│ ├── perry-types/ # Type definitions
│ ├── perry-hir/ # HIR and lowering
│ ├── perry-transform/ # IR passes
│ ├── perry-codegen/ # Cranelift codegen
│ ├── perry-codegen-js/ # Web target codegen
│ ├── perry-codegen-swiftui/ # Widget codegen
│ ├── perry-runtime/ # Runtime library
│ ├── perry-stdlib/ # npm package implementations
│ ├── perry-ui/ # Shared UI types
│ ├── perry-ui-macos/ # macOS AppKit UI
│ ├── perry-ui-ios/ # iOS UIKit UI
│ └── perry-jsruntime/ # QuickJS integration
├── docs/ # This documentation (mdBook)
├── CLAUDE.md # Detailed implementation notes
└── CHANGELOG.md # Version history
Next Steps
- Architecture — Crate map and pipeline overview
- See
CLAUDE.mdfor detailed implementation notes and pitfalls