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.
Footer
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.