Python SDK

Overview

Plexi Python SDK v3 for native ProcessApp apps.

App modules expose exactly three lifecycle functions:

init(size, args) -> list Called once at launch. Return startup effects such as SetTitle or SetState.

update(event) -> list Called for keyboard, mouse, timer, render, and host-result events. Return effects; do not mutate Plexi state directly.

view() -> Component Called after state changes to produce the current component tree. Keep it pure: read state here, but return state-changing effects from update.

Useful entry points: plexi_sdk.effects for effect dataclasses, plexi_sdk.events for event dataclasses, and plexi_sdk.ui for declarative components.

SDK v3 Python apps run as reviewed native processes through ProcessApp. Capabilities gate host APIs; they are not a process sandbox. CPython-in-WASM is deferred and is not this runtime.

Effects

Effect dataclasses returned from a Plexi app’s init() or update().

Effects describe work for the host to perform after the lifecycle function returns. They are data objects, not imperative calls: return [SetTitle("Demo"), SetState({"count": 1})] instead of mutating host state directly. The adapter matches each class name to the PGAP host effect with the same meaning.

SetState

Update process-local runtime state before the next view() call.

data should be a JSON-shaped dict. Values are merged into the runtime snapshot and are not written to durable app storage.

PersistState

Update runtime state and request a durable app-state save.

SetSchedulerMode

Control host-paced render scheduling.

mode is "idle", "scheduled", or "continuous". Continuous mode sends RenderFrame events at fps for games and animations.

SetMouseTracking

Enable or disable mouse-motion events for the pane.

FileRead

Read a workspace-scoped file and receive a FileReadResult event.

FileList

List a workspace-scoped directory and receive a FileListResult event.

FileWrite

Write bytes to a workspace-scoped file through the host.

HttpFetch

Request an HTTP(S) fetch through the host capability gate.

OpenUrl

Ask the host to open an HTTP(S) URL in the default browser.

AiMessage

One chat message in an AiQuery request.

AiQuery

Send a managed AI request and receive chunks/results by request_id.

SetTimer

Schedule a timer; the host replies with TimerFired(id).

CancelTimer

Cancel a timer previously scheduled with SetTimer.

GetSystemStats

Request host system metrics and receive SystemStatsResult.

SetTitle

Set the pane title shown by Plexi chrome.

SetStatus

Set the pane status text shown by Plexi chrome.

CloseSelf

Ask the host to close this app pane.

RequestCapability

Prompt for a named capability grant at runtime.

EventStreamDecl

Declare one structured event stream emitted by the app.

DeclareEventStreams

Register app event streams before emitting events into them.

EmitEvent

Emit one structured app event for host/audit consumers.

Events

Input and host-result events delivered to an app’s update(event).

The ProcessApp adapter converts PGAP input into these dataclasses before it calls update. Apps usually branch with isinstance(event, KeyEvent) or isinstance(event, UiAction) and return a list of effects in response.

Modifiers

Keyboard modifier state attached to KeyEvent.

KeyEvent

Keyboard input. key uses Plexi’s normalized lowercase key names.

MouseEvent

Pointer input in pane coordinates, including buttons, scroll, and region.

UiAction

Click/activation event from a component or host action.

UiValueChange

Value-change event from an editable component with matching handler_id.

ListSelect

Selection changed in a host-rendered list component.

ListActivate

Item activated in a host-rendered list component.

Resize

Pane size changed. Dimensions are logical pixels.

FocusGained

This pane received focus.

FocusLost

This pane lost focus.

FocusChanged

Workspace focus telemetry delivered to apps that consume it.

TimerFired

A SetTimer timer fired; id echoes the scheduled timer id.

RenderFrame

Host-paced render tick for continuous or scheduled apps.

SystemStats

Snapshot of host system metrics returned by GetSystemStats.

SystemStatsResult

Result event for GetSystemStats.

FileReadResult

Result event for FileRead. error is None on success.

FileListEntry

One directory entry returned by FileListResult.

FileListResult

Result event for FileList. entries is None on error.

FileWriteResult

Result event for FileWrite. error is None on success.

HttpResponse

Result event for HttpFetch.

AiStreamChunk

Streaming partial response for an AiQuery request.

AiResponse

Final response for an AiQuery request.

DeclareEventStreamsResult

Result event for DeclareEventStreams.

EmitEventResult

Result event for EmitEvent.

SurfaceReady

GPU surface became available for advanced surface apps.

SurfaceResized

GPU surface size changed for advanced surface apps.

PipePayload

Payload carried by a typed pipe message.

PipeMessage

Typed pipe message delivered from another pane/app.

PipePeerConnected

A peer connected to an opened typed pipe.

PipeClosed

A typed pipe closed.

PipeError

A typed pipe operation failed.

CapabilityGranted

A requested capability was granted.

CapabilityDenied

A requested capability was denied.

PaymentComplete

Payment flow completed for the app.

PaymentFailed

Payment flow failed with a human-readable reason.

State and Logging

StateSnapshot

StateProxy

get(key, default=None)

raw(key)

all()

set(key, value)

LogProxy

debug(msg)

info(msg)

warn(msg)

error(msg)

UI Components

Declarative UI primitives returned from an app’s view().

Build a tree of Component objects such as Column([AppBar(...), Text(...)]) and return it from view(). Components describe what to render; effects from init() and update(event) describe what the host should do. Keep those two concepts separate: view() should read state and return components, not change state or request host work.

Normal apps should use these host-rendered components. Games, simulations, and custom visualizations can return Canvas([...]) and update state from RenderFrame events.

HasToNode

Anything that can serialize itself to a UiNode wire dict.

Component

Base class. Subclasses implement measure and render.

Heading

Title-ish text. level 1 = TEXT_TITLE_XL, 2 = TEXT_TITLE, 3 = TEXT_HEADING.

ctx.text(x, y, ...) treats y as the TOP of the text box (host renders with egui::Align2::LEFT_TOP). A Heading with font size fs occupies rows y .. y + fs plus descender padding.

Label

Body/caption/hint text. Wraps up to max_lines, then truncates.

Line height = font_size + LINE_LEADING; lines stack top-to-bottom with the first line’s top at the component’s y.

Text

Host-rendered text node for labels, counters, and short body copy.

size overrides the default body size. bold and color are inherited from Label. Use Label when you want tone-based body, caption, or hint text; use Text when matching the SDK v3 wire node.

Button

Clickable host-rendered button.

on_click is the handler id delivered back as a UiAction event. Return state/effect changes from update(event) when that event arrives.

ActionBar

Horizontal row of contextual action buttons.

Spacer

Fixed or flex gap. grow=True expands to consume remaining space.

Badge

Small host-rendered pill label for status, shortcuts, and metadata.

Divider

A horizontal 1px rule.

CanvasRect

Rectangle drawing command for Canvas.

CanvasCircle

Circle drawing command for Canvas.

CanvasLine

Line drawing command for Canvas.

CanvasText

Text drawing command for Canvas.

Coordinates are in the canvas coordinate space. Use component Text for normal app UI; use CanvasText only inside a Canvas command list.

CanvasButton

Convenience primitive: a clickable button on a Canvas.

Decomposes into a CanvasRect + CanvasText with a shared hit_region. Use to_commands() (plural) to get the list of underlying primitives.

Canvas

SDK v3 CPU canvas node.

Pass typed drawing commands for host-side rendering from view().

AppBar

Thin top-of-pane app bar with optional subtitle.

Single-line (title only): fixed BAND_H band, title vertically centred. Two-line (title + subtitle): taller BAND_H_DOUBLE band, title/subtitle stacked and centred together.

Section

Section divider with a small uppercase label sitting above the rule.

Vertical stack: SPACE_SM padding, label (TEXT_HINT), SPACE_XS, divider, SPACE_XS padding. The bottom padding is intentionally tight (SPACE_XS instead of SPACE_SM) so the section headline sits close to its associated content block below.

KeyRow

A keycap chip (or a chord of chips) followed by a description, left-aligned.

Emits DrawCommand::KeyChipRow — the host measures each chip with real font metrics and flows them left-to-right. No Python-side width math.

key accepts a single string (e.g. "m") or a list (e.g. ["⌘", "K"]).

ScrollLog

Bounded text log. Shows the most recent lines that fit in the available space; older lines are hidden. Lines are rendered newest-at-top.

Scrollable

A clip-bounded vertically-scrollable container.

Renders its child component clipped to the allocated rect. If the child is taller than the available height the excess is hidden and a thin scrollbar indicator is drawn on the right edge.

Scroll offset is persisted on the instance, so the Scrollable must be stable across renders — create it once at module level or in init(), not inside view().

Keyboard scroll: j/k or arrow-down/up keys update scroll_offset. Apps drive this by calling handle_key(key) from their on_key handler.

Mouse-wheel scroll: call handle_scroll(delta_y) from the app’s on_scroll_delta handler (receives PlexiEvent::Scroll from the host).

ensure_visible(scroll_offset, viewport_h, top, bottom, margin=0.0)

Solve ‘selection follows scroll’ in one call. Returns the new offset.

The pattern: the user is navigating items with j/k. The cursor moves freely while it stays inside the visible viewport; the moment it would go off the top or bottom edge, the viewport scrolls just enough to keep it visible. Identical to every native list widget.

Apps call this from their nav handler after mutating their selected_index — works whether the app uses a Scrollable component or hand-rolls its own scroll offset (commit-graph’s clip-and-offset style):

new_sel = min(self._sel + 1, len(items) - 1)
item_top = new_sel * ROW_H
self._scroll_offset = ensure_visible(
    self._scroll_offset, viewport_h,
    top=item_top, bottom=item_top + ROW_H,
)
self._sel = new_sel

Args: scroll_offset: current scroll offset in the child’s local space. viewport_h: visible height of the viewport. top, bottom: the item’s top/bottom edges in the child’s local space. margin: scrolloff equivalent. Set to one row-height to keep a row of breathing room above/below the cursor.

Returns: The new scroll_offset that keeps [top, bottom] visible. Identical to the input if the cursor was already in view.

Small caption row. Wraps instead of clipping. The parent Column provides the outer bottom padding, so no extra padding is needed here.

FooterKeys

Footer row that renders keyboard shortcuts as key chips + descriptions.

Each shortcut is a (key_or_keys, description) tuple — the same shape as KeyRow. Chips are rendered inline (horizontal flow) separated by a small gap, identical in style to KeyRow but packed tightly so many shortcuts fit on one line.

Example::

FooterKeys([
    ("j", "down"),
    ("k", "up"),
    (["g", "G"], "ends"),
    ("?", "help"),
])

key_or_keys may be a single string or a list of strings (chord). Lists are joined with / as a single chip label so they stay compact in the footer context.

ListItem

Single or double-line list item with optional leading icon and trailing text.

Replaces the manual ctx.rect + y-offset pattern for list rows. All vertical centering is handled internally — no align= juggling or h * 0.38 / h * 0.72 magic numbers needed.

Example::

ListItem(
    title=cmd["name"],
    subtitle=cmd.get("description"),
    trailing="›",
    selected=(i == self._sel),
)

Row

Horizontal row: optional leading icon, main label, optional trailing text.

Vertically centres all items automatically. Use instead of paired ctx.text(x, y + h/2, ..., align="left_center") calls when building info rows with an icon, label, and badge or chevron.

Example::

Row(label="Workspace", leading="⚡", trailing=f"{count}")

badge(ctx, x, y_center, label, fill=None, fg=None, font_size=TEXT_HINT, radius=RADIUS_BADGE)

Render a host-measured pill badge centred on y_center.

The host measures the label with real egui font metrics, sizes the pill (text_w + padding), and centres the text — no Python width math.

Args: ctx: Canvas context. x: Left edge of the badge. y_center: Vertical centre of the badge (e.g. the commit-node cy). label: Text to display inside the pill. fill: Pill background colour. fg: Text colour (default: theme bg — dark text on light pill). font_size: Label pt size (default TEXT_HINT). radius: Corner radius. Use RADIUS_SM (4 px) for tag chips, RADIUS_BADGE (6 px, default) for rounded badges without the perfect-stadium look of RADIUS_MD (8 px).

loading_pill(ctx, x, y, label='Fetching…')

Render a small spinner+label pill at (x, y). Returns rendered width.

The pill uses host-measured badge() rendering (so widths are correct), with a wall-clock-driven Braille spinner glyph that ticks at 8 fps regardless of how often loading_pill is called.

Pattern: position this in the top-right of the region being refreshed. While _fetching is true, render it on top of the stale content. When the fetch completes, just stop calling it.

Args: ctx: Canvas context. x, y: Top-left of the pill (NOT y-centre — easier to anchor). label: Text shown after the spinner glyph.

Card

Surface-colored container with inner padding. Stacks its children vertically with a configurable gap. A 1px border in theme.highlight separates it from the pane background — essential when surface and bg are close in brightness.

TextEdit

Host-rendered text editor. Use inside view() like any other component.

The host maintains a persistent buffer keyed on node_id. Typing fires ComponentEvent with event_type="change" and payload={"value": "..."}; Enter (single-line) or Cmd+Enter (multiline) fires event_type="submit".

height controls the allocated row height (pixels). Default 48.0 suits single-line use; set it larger for multiline (e.g. height=120.0).

Example::

def view(self):
    return Column([
        TextEdit("body", multiline=True, height=120.0, placeholder="Type here..."),
        FooterKeys([("↩", "submit")]),
    ])

ChatBubble

A chat message bubble with left/right alignment and colored background.

align="right" for user messages (accent bg), "left" for assistant messages (surface bg). Error messages use role="error".

Markdown

Host-rendered markdown block for rich read-only text in component trees.

SelectList

Keyboard-navigable scrollable list. Stateful — create at module level or in init().

items: list of dicts with keys: name (str), description (str, optional), leading (str, optional), trailing (str, optional) selected_idx: currently highlighted row index

Call handle_key(key) from on_key. Call hit_index(click_y) from on_click.

FormField

Label + TextEdit row. Create in on_init (stable across renders).

Read .submitted after the render pass; it contains the text entered by the user when they pressed Enter, or None if no submission this frame.

Column

The root container. Stacks children vertically. Handles grow spacers: measures fixed-height children first, then distributes leftover space to any Spacer(grow=True) descendants at the top level.

Padding defaults to SPACE_XL (24px) on the sides and bottom, and SPACE_SM (8px) on the top. Pass padding=0 for full-width content (e.g. apps whose children manage their own horizontal margins). Override top-only with padding_top=.

HStack

Horizontal flex container. Lays children left-to-right, partitioning remaining width to any grow children (Canvas(grow=True), Spacer(grow=True)) after fixed-width siblings (e.g. a Sized sidebar) are subtracted.

Emits a horizontal Stack node so the host width-partitions exactly like a vertical Column height-partitions.

Sized

Explicit-size wrapper. Constrains child to an exact width and/or height. None on an axis means “inherit available”. Primary use: a fixed-width sidebar beside a growing Canvas inside an HStack.

render_tree(ctx, root, fill=None)

Clear the pane to fill, then render root into the full pane rect.

fill defaults to the active host theme background (theme.bg). Apps normally call ctx.render(root) instead, which calls this.

The root component and every descendant must support to_node(). The SDK emits a single ComponentTree command and the host renders it natively.

InfoTable

Key-value table with surface background, border, and row dividers.

Each row is a (key, value) tuple rendered in a fixed-width key column (monospace, green accent) and a value column (monospace, FG).

Example::

InfoTable([
    ("app_id", "my-app"),
    ("workspace", "/path/to/ws"),
])

ButtonRow

A clickable button rendered as a component in the declarative tree.

Use this from view(). Button presses arrive through on_component_event(node_id, event_type, payload).

Example::

self._btn = ButtonRow("action", "Click me")

def view(self):
    return Column([self._btn])

def on_component_event(self, node_id, event_type, payload):
    if node_id == "action" and event_type == "click":
        handle_click()

LeadingBadge

Badge leading slot for :class:ListRow.

Renders a pill badge with label text and the given color.

LeadingAvatar

Circular avatar leading slot for :class:ListRow.

handle must be a UUID returned by emit.load_image(url).

LeadingIcon

Text/emoji icon leading slot for :class:ListRow.

RowChip

A small colored chip label on a :class:ListRow.

ListRow

Typed row descriptor for list views.

Example::

rows = [
    ListRow(
        id=f"issue-{issue['number']}",
        leading=LeadingBadge(f"#{issue['number']}", color="accent"),
        primary=issue["title"],
        chips=[RowChip(lbl["name"], _label_color(lbl["name"])) for lbl in issue["labels"][:2]],
    ).to_dict()
    for issue in self._issues
]
ctx.list_view("issues", rows, selected=self._sel, y=float(HEADER_H))

Tabs

Tabbed container. Renders as a horizontal tab bar + active content area.

Decomposes to a vertical Stack containing:

  • a horizontal Stack of Interactive(Text) tab buttons
  • the active tab’s content node

Example::

tabs = Tabs([
    ("Overview", overview_node),
    ("Details", details_node),
], active=0)
ctx.render_tree(tabs.to_node())

Grid

Fixed-column grid layout.

Decomposes to a vertical Stack of rows, where each row is a horizontal Stack of up to columns children.

Example::

grid = Grid(2, [item_a, item_b, item_c, item_d], gap=8.0)
ctx.render_tree(grid.to_node())

Toggle

On/off toggle switch (L1 sugar).

Renders as an Interactive node with a horizontal stack indicator.

Example::

toggle = Toggle("dark_mode", value=True, label="Dark mode")
ctx.render_tree(toggle.to_node())

Clickable

Makes any component clickable by wrapping it in an Interactive node.

Example::

clickable = Clickable("my_btn", child_node)
ctx.render_tree(clickable.to_node())

ProgressBar

Horizontal progress bar (L0 decomposition).

Decomposes to a horizontal Stack with a filled portion and an empty portion sized proportionally to value / max_value.

Example::

bar = ProgressBar(0.75, color="accent")
ctx.render_tree(bar.to_node())

Testing

Headless snapshot testing for Plexi widgets.

Spawns the plexi binary with --render, feeds it DrawCommand JSON, reads back PNG bytes. Provides pixel-level assertions and snapshot file helpers.

render_draw_commands(commands, width, height, background=DEFAULT_BG)

Feed draw commands to the headless renderer, return PNG bytes.

Args: commands: list of PGAP draw command dicts (rect, text, line, …). width: viewport width in pixels. height: viewport height in pixels. background: CSS hex colour string for the background fill.

Returns: Raw PNG bytes.

Raises: RuntimeError if the binary is not found or exits nonzero.

decode_png(png_bytes)

Decode an RGBA 8-bit PNG to (width, height, raw_rgba_bytes).

Pure stdlib implementation using zlib + struct. Only handles RGBA 8-bit PNGs — which is exactly what tiny-skia produces.

Raises: ValueError on an unrecognised PNG or unsupported colour type/bit depth.

pixel_at(png_bytes, x, y)

Return (r, g, b, a) of the pixel at (x, y).

hex_to_rgba(hex_color)

Convert a CSS hex color string to (r, g, b, a).

Supports #RGB, #RRGGBB, and #RRGGBBAA. Alpha defaults to 255 when not specified.

assert_pixel(png_bytes, x, y, expected, tolerance=4)

Assert the pixel at (x, y) matches the expected color within per-channel tolerance.

Args: png_bytes: raw PNG bytes from render_draw_commands. x, y: pixel coordinates. expected: CSS hex string (e.g. “#ff0000”) or (r, g, b, a) tuple. tolerance: per-channel absolute tolerance (default 4).

Raises: AssertionError with a diagnostic message on mismatch.

save_snapshot(png_bytes, path)

Write PNG bytes to path. Creates parent directories as needed.

AppHarness

Headless runner for Plexi Python apps.

Spawns the app script as a subprocess, communicates over stdin/stdout using the PGAP v3 protocol. No display or running Plexi host required.

Usage::

with AppHarness("my_app.py", width=400, height=300) as h:
    cmds = h.run(1)                  # step one render frame
    h.key("enter")                   # inject a key event
    cmds = h.run(1)                  # render again to see effects
    h.assert_pixel(10, 10, "#1e1e2e")  # pixel assertion (needs plexi binary)

Types

CapabilityDeniedError

Raised when the host rejects a brokered call because the app’s manifest didn’t declare the required capability. Distinct from generic RuntimeError so apps can catch the gate-denial path explicitly.

VideoHandle

Video handle. handle_id is opaque, passed back to video control effects. pipe delivers decoded RGBA8 frames of length width * height * 4.

RectCommand

Typed constructor for ctx.rect(). Validates geometry at construction.

TextCommand

Typed constructor for ctx.text(). Validates align at construction.

BadgeCommand

Typed constructor for ctx.badge().

ShortcutPair

Typed constructor for one entry in ctx.shortcuts() pairs.

NotifyOption

Typed constructor for one option in ctx.notify_choice().

Theme

Live theme singleton.

Populated from the host Init payload so app chrome tracks the host theme (light/dark + user overrides in config.toml). Until Init arrives the attributes hold the built-in dark defaults.

Process-wide instance theme is mutated in place on Init. Never rebind the name, only set attributes.

theme.is_dark is True when the background luminance is below 0.5.

Theme

Mutable bag of semantic color roles, each a #rrggbb string.

is_dark is a computed boolean: True when the background luminance is below 0.5 (i.e. the theme is dark-mode). Derived from bg on every update_from call so it stays in sync with theme hot-reloads.

__init__()

reset()

update_from(payload)

Overlay host-provided roles. Unknown keys and non-string/empty values are ignored so a partial payload never blanks a color.

AppPalette

Light/dark palette for app-defined color tokens.

Both dicts must have the same keys. resolve(theme) returns the matching set based on theme.is_dark.

__init__(dark, light)

resolve(theme)

Return the dark or light token set based on theme.is_dark.

Constants

rgba(r, g, b, a=255)

Return an 8-digit hex color string #rrggbbaa.

dim(hex_color, alpha)

Return hex_color with the given alpha (0-255). Strips existing alpha.

Protocol Types

AiResponse

Result of Emitter.ai_query. tokens_in/tokens_out are zero on error.

MidiPortInfo

One MIDI port. Mirrors MidiPortWire in the Rust protocol.

MidiDeviceList

Result of Emitter.list_midi_devices.

AudioDeviceInfo

One audio device. Mirrors AudioDeviceWire in the Rust protocol.

AudioDeviceList

Result of Emitter.list_audio_devices.