Automation
Run a program under automation with pb run --script:
# script from a file
pb run program.wasm --script script.jsonl
# inline JSONL argument (first character '{'; may contain multiple lines)
pb run program.wasm --script '{"command":"screenshot","id":"a","format":"png"}'
# one JSONL line per argument
pb run program.wasm --script -- '{"command":"fence"}' '{"command":"screenshot","id":"a"}'
# JSONL lines on stdin
printf '%s\n' '{"command":"screenshot","id":"a"}' | pb run program.wasm --script -
<input> can also be a source file or directory; pb builds it first. The run is
synchronous: screenshot results are written to pb's stdout, logs go to stderr,
and Playbit.app's exit status propagates to pb run.
pb run --script drives Playbit.app's remote-control mode, which can also be
used directly:
/Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm
--script is accepted as an alias for --remote-control.
In this mode:
- stdin is a JSONL command stream
- stdout is a JSONL result stream for
screenshotcommands only - stderr is for logs and fatal protocol errors
- commands are processed in input order
One JSON object per line.
Fatal Errors
The process terminates with an error on stderr when:
- a line is not valid JSON
commandis missing or invalid- a non-
screenshotcommand has invalid fields - a non-
screenshotcommand cannot be enqueued or synchronized - a nonzero
objectIddoes not match the main window
screenshot is the only recoverable command. It writes either a success result or an error result
to stdout.
Window Readiness
There is no window_opened event.
Commands that require a window are held until the main window is:
- created
- visible
- has presented at least one frame
Window-dependent commands:
fencescreenshotpointer_enterpointer_leavepointer_downpointer_uppointer_movepointer_cancelscrollgesture_pangesture_pinchgesture_rotatekey_downkey_up
wait does not require a window.
Queue Semantics
wait
- delays later commands by
ms - uses a timer; it does not block a thread while waiting
fence
- waits until all prior inserted input has been consumed by the guest
- if prior commands caused a frame request, waits for the resulting frame to present
- produces no stdout output
- is not needed immediately before
screenshot, sincescreenshotalready implies the same wait
Event commands
- enqueue the requested runtime event
- do not wait for guest consumption or frame presentation
- produce no stdout output
screenshot
- first waits until all prior inserted input has been consumed by the guest
- for
format:"png", waits for any resulting frame to present - for
format:"json", waits for the guest barrier after prior input so the latest submitted canvas command stream reflects the effects of those commands - then captures a PNG or writes a JSON canvas-command description
- writes one JSON result line to stdout
- this means
{"command":"key_down",...}followed by{"command":"screenshot",...}captures state after the key event has been applied; PNG additionally waits for any resulting frame to present before capture - an explicit
fenceimmediately beforescreenshotis redundant
When stdin is closed, the app exits after the last queued command completes.
Command Shape
All input lines have this base shape:
interface InputMessage {
command: string
id?: string
}
id is opaque. It is only echoed back by screenshot results.
Wait
{"command":"wait","ms":250}
msis a non-negative integer number of milliseconds
Fence
{"command":"fence"}
Screenshot
{"command":"screenshot","id":"s1","rect":{"x":0,"y":0,"width":640,"height":480}}
By default (no format), a screenshot writes three files and reports all
three paths in the result:
path— the OS window screenshot: what the user sees on screen, captured by the OS and encoded with the color profile of the display the window is on.rectapplies to this file only.render_path(<stem>.render.tiff) — a byte-exact capture of the render output: the full surface in native pixels, read back from the GPU with no OS compositing and no color conversion. The file is an uncompressed TIFF with four 16-bit IEEE-float samples per pixel (the render target's RGBA16Float texels verbatim), tagged extended linear Display P3. 1.0 is SDR white; values above 1.0 are HDR and negative values are outside the P3 gamut. Tests can assert exact binary16 values (see read_tiff_pixels in host-app/tests/test_remote_control.py).commands_path(<stem>.commands.json) — a JSON description of the most recently submitted canvas commands (see file format below).
Fields:
output_fileoptional output path (the OS screenshot; the other two paths are derived from its stem)formatoptional,"png"or"json": single-file modes writing only the OS screenshot or only the commands JSON, reported aspathrectoptional capture rect in window content coordinates, top-left origin
Rules:
- If
output_fileis omitted or"", files are created in_pb/screenshots/(relative to the process working directory) namedYYMMDD-HHMMSS.usec.<ext>; the result reports absolute paths. rectis clipped against the window content area; it does not apply to the render output or commands JSON.- The render output capture re-encodes the most recently submitted canvas command buffers; it reflects the latest submission, which the screenshot fence guarantees has been presented.
- JSON screenshot files also include
windowObjectId, the main window's object id. This is useful when testing explicit nonzeroobjectIdinput.
JSON screenshot file format:
interface CanvasCommandScreenshot {
windowObjectId: number
dpScale: number
shapes: ShapeItem[]
texts: TextItem[]
instructions: InstructionItem[]
clips: ClipItem[]
}
interface Vec4 {
x: number
y: number
z: number
w: number
}
interface Vec2 {
x: number
y: number
}
interface F32x4 {
x: number
y: number
z: number
w: number
}
interface ShapeItem {
bounds: Vec4
transformX: Vec2
transformY: Vec2
transformO: Vec2
cornerRadius: F32x4
fillColor: string
fillColors: [string, string, string, string]
strokeColor: string
strokeWidth: number
flags: number
blur: number
opacity: number
}
interface TextItem {
bounds: Vec4
transformX: Vec2
transformY: Vec2
transformO: Vec2
textPlan: number
}
// One entry per submitted command, in stream order. Coordinates are raw
// submission-space values (multiply points by dpScale to compare with the
// point-converted shapes/texts/clips arrays). Colors are wire colors dumped
// verbatim as hex strings: four binary16 channels in r,g,b,a memory order,
// so the u64 reads 0xAAAABBBBGGGGRRRR.
interface InstructionItem {
kind:
| "rect"
| "rounded_rect"
| "stroke_rect"
| "complex_rect"
| "gradient_rect"
| "text"
| "clip_push"
| "clip_pop"
| "texture_set"
| "transform_push"
| "transform_pop"
| "group_push"
| "group_pop"
| "group_mask"
| "gradient"
| "mesh_gradient"
bounds?: { x1: number, y1: number, x2: number, y2: number }
fillColor?: string
fillColors?: [string, string, string, string] // gradient_rect
strokeColor?: string
strokeWidth?: number
strokeFlags?: number
cornerRadius?: F32x4
opacity?: number
blur?: number // complex_rect; group_push (gaussian radius, 0..100)
uvMin?: Vec2 // complex_rect
uvMax?: Vec2 // complex_rect
textPlan?: number // text
flags?: number // text
texture?: number // texture_set
transformX?: Vec2 // transform_push
transformY?: Vec2 // transform_push
transformO?: Vec2 // transform_push
blendMode?: number // group_push (PBSysCanvasBlendMode)
maskFlags?: number // group_mask (PBSysCanvasMaskFlags)
// gradient (see PBSysCanvasCmdGradient): flags is the raw cmd.flags value
gradientKind?: number // PBSysCanvasGradientKind
space?: number // PBSysCanvasGradientSpace
spread?: number // PBSysCanvasGradientSpread
p0?: Vec2
p1?: Vec2
stops?: { offset: number, color: string }[]
// mesh_gradient (see PBSysCanvasCmdMeshGradient); space as for gradient
falloff?: number
points?: { x: number, y: number, weight: number, color: string }[]
}
interface ClipItem {
bounds: Vec4
transformX: Vec2
transformY: Vec2
transformO: Vec2
}
Notes:
boundsis the computed screen-space axis-aligned bounding box{x:minX,y:minY,z:maxX,w:maxY}.- Text and clip items are encoded with affine transforms matching canvas renderer commands.
cornerRadiusis encoded as four 32-bit float values{x,y,z,w}.- Color values are hex strings like
"0xFF18120C00000000". instructionsis reserved for compatibility and is currently empty.textPlanis the runtime text-plan handle captured when the canvas command was submitted.- The JSON file is intended for testing and automation. It reflects the latest submitted renderer canvas commands, not a rasterized image.
Example:
{
"windowObjectId": 123,
"shapes": [
{
"bounds": { "x": 24, "y": 24, "z": 40, "w": 40 },
"transformX": { "x": 16, "y": 0 },
"transformY": { "x": 0, "y": 16 },
"transformO": { "x": 24, "y": 24 },
"cornerRadius": { "x": 0, "y": 0, "z": 0, "w": 0 },
"fillColor": "0xFFC4E87000000000",
"fillColors": [
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"strokeColor": "0x0000000000000000",
"strokeWidth": 0,
"flags": 0,
"blur": 0,
"opacity": 1
}
],
"texts": [
{
"bounds": { "x": 24, "y": 64, "z": 104, "w": 92 },
"transformX": { "x": 1, "y": 0 },
"transformY": { "x": 0, "y": 1 },
"transformO": { "x": 24, "y": 64 },
"textPlan": 10
}
],
"instructions": [],
"clips": [
{
"bounds": { "x": 18, "y": 250, "z": 364, "w": 360 },
"transformX": { "x": 330, "y": 36 },
"transformY": { "x": -16, "y": 74 },
"transformO": { "x": 34, "y": 250 }
}
]
}
Stdout result:
interface ScreenshotSuccessResult {
id?: string
path: string
}
interface ScreenshotErrorResult {
id?: string
error: string
}
Examples:
{"id":"s1","path":"/path/to/project/_pb/screenshots/260806-143502.123456.png"}
{"id":"s1","error":"no canvas commands available"}
Event Commands
Common fields for all event commands:
interface EventCommand extends InputMessage {
objectId?: number
timestamp?: number
clientId?: number
deviceId?: number
modifiers?: string
}
Defaults:
objectId:0timestamp: currentPBSysTimeclientId:0deviceId:0modifiers:""
objectId handling:
0targets the main window and leaves the inserted eventobjectIdas0- nonzero
objectIdmust equal the main window object id
modifiers is a space-separated list of:
shiftctrlaltmetacapslockfn
Pointer Events
Commands:
pointer_enterpointer_leavepointer_downpointer_uppointer_movepointer_cancel
Shape:
interface PointerEventCommand extends EventCommand {
command:
| "pointer_enter" | "pointer_leave" | "pointer_down"
| "pointer_up" | "pointer_move" | "pointer_cancel"
pointerId?: number
flags?: string
buttons?: number[]
button?: number
kind?: "mouse" | "touch" | "trackpad" | "pen"
clickCount?: number
x: number
y: number
dx?: number
dy?: number
}
Defaults:
pointerId:0flags:""buttons:[]button: omitted, encoded as0kind:"mouse"clickCount:0dx:0dy:0
flags is a space-separated list of:
primaryin_contacteraserinvertedcoalescedpredicted
buttons and button use 1-based numbering in JSON:
1primary2secondary3middle4+additional buttons
Example:
{"command":"pointer_move","x":120,"y":80,"dx":4,"dy":-2,"flags":"primary"}
Scroll
interface ScrollCommand extends EventCommand {
command: "scroll"
kind?: "mouse" | "touch" | "trackpad" | "pen"
phase?: "changed" | "began" | "ended" | "momentum"
flags?: string
x?: number
y?: number
dx: number
dy: number
wheelZ?: number
}
Defaults:
kind:"trackpad"phase:"changed"flags:""x:0y:0wheelZ:0
flags is a space-separated list of:
preciseinvertedunit_linesunit_pages
Example:
{"command":"scroll","x":120,"y":220,"dx":0,"dy":-48,"flags":"precise"}
Gestures
gesture_pan
{"command":"gesture_pan","phase":"changed","x":120,"y":220,"dx":16,"dy":0}
gesture_pinch
{"command":"gesture_pinch","phase":"changed","x":120,"y":220,"scale":1.05}
gesture_rotate
{"command":"gesture_rotate","phase":"changed","x":120,"y":220,"rotation":0.1}
Shared fields:
phaseoptional:"changed","began","ended", default"changed"x,yoptional, default0,0
gesture_pan defaults dx=0, dy=0.
gesture_pinch defaults scale=1.0.
gesture_rotate defaults rotation=0.
Keyboard
Commands:
key_downkey_up
Two payload forms are supported.
Keyed form:
{"command":"key_down","key":"Tab","deviceKey":"Tab"}
IME/text form:
{"command":"key_down","text":"a"}
Shape:
interface KeyboardEventCommand extends EventCommand {
command: "key_down" | "key_up"
flags?: string
}
interface KeyedKeyboardEventCommand extends KeyboardEventCommand {
key: string
deviceKey: string
}
interface TextKeyboardEventCommand extends KeyboardEventCommand {
command: "key_down"
text: string
}
Rules:
flagsis a space-separated list containingrepeat- keyed and text forms are mutually exclusive
- text form is only valid for
key_down textmay contain up to 8 Unicode codepoints- in text form,
keyCodeanddeviceCodeare derived from the first codepoint
Keyboard key names are case-insensitive and use the PBSysKeyboardKey variant suffix name, for
example:
"LeftBracket""Enter""g""G"
Examples
Click and take a screenshot:
pb run program.wasm --script -- \
'{"command":"pointer_move","x":120,"y":80}' \
'{"command":"pointer_down","x":120,"y":80,"buttons":[1],"button":1}' \
'{"command":"pointer_up","x":120,"y":80,"buttons":[],"button":1}' \
'{"command":"screenshot","id":"shot1"}'
Delay before a screenshot:
pb run program.wasm --script -- \
'{"command":"wait","ms":250}' \
'{"command":"screenshot","id":"shot2","format":"png"}'
Write the latest render commands as JSON:
pb run program.wasm --script \
'{"command":"screenshot","id":"pkg1","format":"json","output_file":"/tmp/render-package.json"}'
Pipe a whole command stream to Playbit.app directly:
{
printf '%s\n' '{"command":"pointer_move","x":120,"y":80}'
printf '%s\n' '{"command":"screenshot","id":"shot1"}'
} | /Applications/Playbit.app/Contents/MacOS/Playbit \
--remote-control program.wasm