plexi-sdk 0.4.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
plexi_sdk/__init__.py ADDED
@@ -0,0 +1,477 @@
1
+ """
2
+ plexi_sdk — Plexi external app SDK (Python), PGAP v3
3
+
4
+ Spec: docs/specs/releases/plexi-v3.0.md §3 (PGAP v3), §7 (typed pipes).
5
+ Zero dependencies, pure stdlib.
6
+
7
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8
+ QUICK START
9
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10
+
11
+ from plexi_sdk import App, BG, FG, BODY, ACCENT
12
+
13
+ class CounterApp(App):
14
+ async def on_init(self, ctx):
15
+ # Called once after the host completes the Init handshake.
16
+ # ctx.workspace_root, ctx.capabilities, ctx.feature_flags are set.
17
+ # Hooks may be async def (to use await) or plain def (fire-and-forget).
18
+ self.count = 0
19
+ self.emit.info("CounterApp ready")
20
+ # Blocking helpers are coroutines — await them directly:
21
+ # api_key = await self.emit.secret_get("MY_API_KEY")
22
+
23
+ def on_render(self, ctx):
24
+ # Pure-sync render hooks work unchanged — no await needed here.
25
+ # ctx.w / ctx.h are the current pane dimensions.
26
+ # ctx.elapsed is seconds since the previous render (0.0 on first frame).
27
+ ctx.clear(BG)
28
+ ctx.rect(20, 20, ctx.w - 40, 60, fill="#313244", radius=8.0)
29
+ ctx.text(36, 42, f"Count: {self.count}", size=BODY, color=FG)
30
+ ctx.text(36, 72, "Press +/- to change • q to quit", size=12.0, color="#6c7086")
31
+
32
+ def on_key(self, ctx, key, mods):
33
+ # key is a string: "a"-"z", "up", "down", "left", "right",
34
+ # "return", "escape", "backspace", "tab", "space", "f1"…"f12", etc.
35
+ # mods shape: {"shift": bool, "ctrl": bool, "alt": bool, "meta": bool}
36
+ if key == "+" or (key == "=" and mods.get("shift")):
37
+ self.count += 1
38
+ elif key == "-":
39
+ self.count -= 1
40
+ elif key == "q":
41
+ pass # host handles quit; apps cannot self-exit
42
+
43
+ def on_click(self, ctx, x, y, button):
44
+ # button: "primary" | "secondary" | "middle"
45
+ # x, y are pixel coordinates within the pane
46
+ ctx.notify("Clicked", priority=50, body=f"({x:.0f}, {y:.0f}) {button}")
47
+
48
+ CounterApp().run()
49
+
50
+
51
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52
+ PROTOCOL OVERVIEW (PGAP v3)
53
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
54
+
55
+ Newline-delimited JSON over stdin/stdout. Binary data travels on typed Unix
56
+ socket pipes, not stdio.
57
+
58
+ PlexiEvent (host → app):
59
+ Init — handshake; delivers app_id, workspace_root, capabilities,
60
+ feature_flags, and protocol version string ("pgap/3.x")
61
+ Render — draw a new frame; carries frame_id and rect {x,y,w,h}
62
+ Key — keypress; carries key string and modifiers dict
63
+ Click — pointer event; carries x, y, button string
64
+ Command — command-palette entry submitted by the user; carries text
65
+ CapabilityDecision — response to a CapabilityRequest; carries request_id and granted bool
66
+ SecretValue — response to SecretGet; carries key and value (str or null)
67
+ HttpResponse — response to HttpRequest; carries request_id, body, and optional error
68
+ RunUpdate — streaming update from a RunGet job; carries run_id and payload
69
+ PipeMessage — JSON-mode pipe message; carries pipe_id and payload
70
+ PipeOpened — binary pipe ready; carries pipe_id and socket_path (Unix socket)
71
+ PipeOverrun — host dropped frames on a pipe; carries pipe_id and dropped_frames count
72
+ PathChanged — terminal cwd broadcast; carries cwd string
73
+ PaneSpawned — confirmation that a SpawnPane request completed; carries pane_id
74
+ PaneSpawnError — SpawnPane could not be fulfilled; carries reason
75
+ InjectState — host-initiated state injection; carries payload dict
76
+ Suspend — app is being hidden/backgrounded
77
+ Resume — app is visible again
78
+ Shutdown — app should clean up and exit
79
+
80
+ DrawCommand (app → host):
81
+ Rect — filled rectangle with optional corner radius
82
+ Circle — filled circle
83
+ Text — text label with font size, color, monospace/bold flags
84
+ Line — straight line segment
85
+ List — scrollable item list (see ListItem shape below)
86
+ Image — display a raster image by path or data URI
87
+ VideoPlayer — embed a video player widget
88
+ AudioMeter — display a real-time audio level meter
89
+ AudioPlay — play audio from a file or pipe
90
+ AudioCapture — open an audio capture stream
91
+ FrameDone — signals end of a render frame (auto-sent by SDK; do not call manually)
92
+ Log — structured log line forwarded to the host log
93
+ Notify — trigger a system notification
94
+ CapabilityRequest — request a runtime capability; host may prompt the user
95
+ SecretGet — request a secret by key from the host secrets store
96
+ HttpRequest — broker an HTTP request through the host (requires net.http capability)
97
+ RunGet — dispatch an intent-based AI/agent job
98
+ RunComplete — mark a RunGet job as finished
99
+ PipeOpen — open a typed pipe (json or binary, in/out/duplex)
100
+ PipeSend — send a JSON payload on a json-mode pipe
101
+ StatusSummary — set the status bar summary text for this pane
102
+ ScheduleRender — ask the host to send a Render event after N milliseconds
103
+ SpawnPane — request the host to open a pane with given app, layout, args, and optional pipe_id
104
+ CdRequest — request the host to cd all terminals in the pane group to a path
105
+ Ready — sent automatically after Init; do not emit manually
106
+
107
+
108
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
109
+ THEME CONSTANTS
110
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
111
+
112
+ Font sizes (float, points):
113
+ TITLE = 22.0 — primary heading
114
+ HEADING = 18.0 — section heading
115
+ BODY = 15.0 — default body text
116
+ CAPTION = 13.0 — secondary label
117
+ HINT = 12.0 — muted hint text
118
+ MONO_BODY = 14.0 — monospace body (code)
119
+ MONO_SMALL = 12.0 — monospace small (log output)
120
+
121
+ Layout (float, pixels):
122
+ PAD = 16.0 — standard outer padding
123
+ PAD_TIGHT = 8.0 — tight/inner padding
124
+ HEADER_H = 48.0 — standard header bar height
125
+ STATUS_H = 44.0 — status bar height
126
+
127
+ Colors (hex strings, Catppuccin Mocha):
128
+ BG = "#1e1e2e" — main background
129
+ SURFACE = "#313244" — elevated surface / card
130
+ HIGHLIGHT = "#45475a" — hover / selection highlight
131
+ ACCENT = "#89b4fa" — primary accent (blue)
132
+ MUTED = "#6c7086" — muted / disabled text
133
+ FG = "#cdd6f4" — primary foreground text
134
+ RED = "#f38ba8" — error / destructive
135
+ GREEN = "#a6e3a1" — success / positive
136
+ YELLOW = "#f9e2af" — warning / caution
137
+
138
+ Color helpers:
139
+ rgba(r, g, b, a=255) -> str — build an 8-digit hex string #rrggbbaa
140
+ dim(hex_color, alpha) -> str — apply alpha (0-255) to an existing hex color
141
+
142
+
143
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
144
+ NOTIFICATIONS
145
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
146
+
147
+ Eight methods across two groups: blocking (await) and non-blocking (callback).
148
+
149
+ Blocking — await these from async hooks, or call via emit.run_sync() from threads:
150
+
151
+ ctx.notify(title, priority, body="", level="info")
152
+ Fire-and-forget message. Enter / Space acknowledge, Esc dismisses.
153
+
154
+ ctx.notify_and_wait(title, priority, body="") -> str
155
+ Same as notify() but blocks. Returns "acknowledge" or "cancel".
156
+
157
+ ctx.notify_choice(title, options, priority, body="", required=False) -> str
158
+ Blocking choice picker. options = [{"label":..., "value":...,
159
+ "shortcut":...}]. Returns chosen value (or label if no value),
160
+ or "__cancel__" if dismissed.
161
+
162
+ ctx.notify_input(title, priority, prompt="", body="", required=False) -> str
163
+ Blocking text input. Returns the typed string, or "__cancel__".
164
+
165
+ ctx.notify_with_image(title, body, image_bytes, mime, priority,
166
+ level="info", choices=None) -> str | None
167
+ Convenience wrapper that handles base64 encoding + 50 KB cap.
168
+ `image_bytes` > 50 KB raises ValueError locally. With `choices=None`
169
+ this is fire-and-forget (returns None); with `choices` set it routes
170
+ to notify_choice and blocks for the user's pick. `mime` must be
171
+ "image/png" or "image/jpeg".
172
+
173
+ Non-blocking (#310) — return immediately with notify_id (str); `on_response`
174
+ callback fires on the event thread when the user responds. No worker thread
175
+ needed. The callback registry is cleaned up after first invocation.
176
+
177
+ ctx.notify_async(title, priority, body="", on_response=None) -> str
178
+ Non-blocking message. on_response=None → pure fire-and-forget (returns "").
179
+ on_response=fn → callback receives "acknowledge" or "__cancel__".
180
+
181
+ ctx.notify_and_wait_async(title, priority, body="", on_response=None) -> str
182
+ Non-blocking variant of notify_and_wait. Callback receives "acknowledge"
183
+ or "__cancel__".
184
+
185
+ ctx.notify_choice_async(title, options, priority, body="", on_response=None) -> str
186
+ Non-blocking variant of notify_choice. Callback receives the chosen value
187
+ (or label), or "__cancel__".
188
+
189
+ ctx.notify_input_async(title, priority, prompt="", body="", on_response=None) -> str
190
+ Non-blocking variant of notify_input. Callback receives typed text or
191
+ "__cancel__".
192
+
193
+ Image attachments (#74) — pass `image_inline={"mime", "base64"}` to any of
194
+ the notify* methods, or use `notify_with_image` for the convenience wrap.
195
+ Inline images cap at 50 KB decoded; oversized images render a placeholder
196
+ badge instead of the bitmap. The `image_pipe_id` field is reserved for a
197
+ future host-side rendering primitive — apps cannot publish frames through
198
+ it today. Use the inline path for now.
199
+
200
+ Priority — required kwarg on every call. Use the named constants:
201
+
202
+ PRIORITY_LOW = 0 Background info. Stacks at the bottom of the queue.
203
+ PRIORITY_NORMAL = 50 Standard confirmations — "note saved", "done", etc.
204
+ PRIORITY_HIGH = 100 Needs attention soon — not blocking but noticeable.
205
+ PRIORITY_CRITICAL = 200 Interrupt-level. Use sparingly; reserve for user
206
+ decisions the app genuinely depends on. If every
207
+ notification is CRITICAL, none is.
208
+
209
+ (A future version may reserve a user-only priority band above CRITICAL so
210
+ a misbehaving app can't yell itself to the top of someone's queue. Apps
211
+ should stay under 200 regardless; 0..200 is the app band.)
212
+
213
+ Queue model:
214
+
215
+ - Notifications pile into a single priority-sorted queue (priority DESC,
216
+ arrival ASC). The front-most is pinned by id — new notifications
217
+ arriving NEVER change what's on screen, only the total count.
218
+ - On dismiss, the next front-most is chosen dynamically from whatever's
219
+ in the queue right now — not from a pre-frozen snapshot.
220
+ - Cmd+] / Cmd+[ preview other queued notifications without acknowledging.
221
+ Cmd+Shift+A toggles the modal on/off.
222
+
223
+ Scope — window / context / global — is NOT a runtime choice. It's declared
224
+ per-app in the app's manifest.toml under [launch]:
225
+
226
+ [launch]
227
+ notification_scope = "global" # "window" | "context" | "global"
228
+
229
+ "window" — default. Visible only when the app's window is active.
230
+ No behaviour change for apps that omit the field.
231
+ "context" — visible whenever the user is in the same sidebar project,
232
+ regardless of which window page is showing.
233
+ "global" — always visible across all contexts (use for stand-up
234
+ reminders, timers, monitoring dashboards).
235
+
236
+ The user controls which scope a given app uses by editing its manifest.
237
+ Apps do not see or set scope at the SDK level.
238
+
239
+ Round-trip response — the blocking helpers (notify_choice / notify_input /
240
+ notify_and_wait) park a queue and await the response. The _async variants
241
+ register an on_response callback instead — no thread required, no await
242
+ needed. Both paths use the same host-side notify_id / NotifyAction plumbing.
243
+
244
+
245
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
246
+ MANIFEST REFERENCE (examples/<app>/manifest.toml)
247
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
248
+
249
+ Required:
250
+ [app]
251
+ id = "my-app" # stable identifier — used for launch slot, log
252
+ # target "app::<id>", install dir, pack refs
253
+ name = "My App" # human-readable display name
254
+ version = "0.1.0"
255
+ description = "…"
256
+ entry = "my_app.py" # executable entry point, relative to manifest
257
+
258
+ Optional:
259
+ [launch]
260
+ notification_scope = "context" # "window" (default) | "context" | "global"
261
+
262
+ [app.capabilities]
263
+ capabilities = [] # e.g. ["net.http", "audio.record"]
264
+ # apps must declare what they use; host prompts
265
+ # on install (future) and gates at runtime
266
+
267
+ [launch]
268
+ layout_hint = { side = "above", split = 0.5 } # preferred split direction
269
+ # + size when spawned
270
+
271
+
272
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
273
+ RenderContext (ctx passed to on_init, on_render, on_key, on_click, …)
274
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
275
+
276
+ Attributes:
277
+ ctx.x, ctx.y — pane origin in logical pixels (usually 0, 0)
278
+ ctx.w, ctx.h — pane width and height in logical pixels
279
+ ctx.frame_id — monotonically increasing render counter
280
+ ctx.elapsed — seconds since previous on_render (0.0 on first frame)
281
+ ctx.workspace_root — absolute path to the workspace root directory
282
+ ctx.capabilities — list of granted capability strings
283
+ ctx.feature_flags — list of enabled feature flag strings
284
+ ctx.emit — Emitter instance (same as self.emit on App)
285
+
286
+ Drawing methods:
287
+ ctx.clear(fill)
288
+ Fill the entire pane with a solid color. Equivalent to ctx.rect(0, 0, w, h, fill).
289
+
290
+ ctx.rect(x, y, w, h, fill, radius=0.0)
291
+ Draw a filled rectangle. radius > 0 rounds the corners.
292
+
293
+ ctx.circle(cx, cy, r, fill)
294
+ Draw a filled circle centered at (cx, cy) with radius r.
295
+
296
+ ctx.text(x, y, text, size, color, monospace=False, bold=False)
297
+ Draw a text label. x, y are the top-left origin of the text block.
298
+
299
+ ctx.line(x1, y1, x2, y2, color, width=1.0)
300
+ Draw a straight line segment.
301
+
302
+ ctx.list_view(items, selected=0, item_height=40.0, x=0, y=0, w=None, h=None)
303
+ Draw a scrollable list. w defaults to ctx.w; h defaults to ctx.h - y.
304
+ Each item is a dict — see ListItem shape below.
305
+
306
+ Notification / logging (usable inside or outside a frame):
307
+ ctx.notify(title, priority, body="", level="info", actions=None)
308
+ Trigger a system notification. actions: list of NotifyAction dicts (see below).
309
+
310
+ ctx.status_summary(text)
311
+ Set the status bar summary text for this pane.
312
+
313
+ ctx.log(level, message) / ctx.info(msg) / ctx.warn(msg)
314
+ ctx.error(msg) / ctx.debug(msg)
315
+ Forward a log line to the host logger, tagged with this app's ID.
316
+
317
+
318
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
319
+ Emitter (self.emit — available at all times, including background threads)
320
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
321
+
322
+ All methods are thread-safe (protected by a global write lock).
323
+
324
+ emit.notify(title, priority, body="", level="info", actions=None)
325
+ Trigger a system notification outside of a render frame.
326
+
327
+ emit.log(level, message) / emit.info(msg) / emit.warn(msg)
328
+ emit.error(msg) / emit.debug(msg)
329
+ Write a structured log line to the host log.
330
+
331
+ emit.status_summary(text)
332
+ Set the status bar summary text for this pane.
333
+
334
+ emit.schedule_render(after_ms=16)
335
+ Ask the host to send a Render event after after_ms milliseconds.
336
+ Use at the end of on_render to drive a continuous animation loop.
337
+ 16 ms ≈ 60 fps | 32 ms ≈ 30 fps.
338
+
339
+ emit.secret_get(key) -> str | None [BLOCKING]
340
+ Request a secret by key from the host secrets store. Blocks until the
341
+ host responds. Returns the secret string, or None if denied/not found.
342
+
343
+ emit.http_get(url) -> str [BLOCKING]
344
+ Broker an HTTP GET through the host. Requires the net.http capability.
345
+ Blocks until the response arrives. Raises RuntimeError on failure.
346
+ Call from a background thread to avoid stalling the render loop.
347
+
348
+ emit.ai_query(model_tier, system, messages, tools=None) -> AiResponse [BLOCKING]
349
+ Plexi AI broker call (#284). Requires the `ai.query` capability declared
350
+ in manifest.toml. `model_tier` is "low" | "medium" | "high"
351
+ (Haiku / Sonnet / Opus). Returns an AiResponse with content, tokens_in,
352
+ tokens_out. Raises CapabilityDeniedError if the manifest didn't grant
353
+ `ai.query`, or RuntimeError on any other backend failure. Call from a
354
+ background thread — the host may take seconds to reply.
355
+
356
+ emit.capability_request(capability) -> None [BLOCKING]
357
+ Request a runtime capability (e.g. "net.http", "fs.write"). The host
358
+ may show a permission prompt to the user. Blocks until granted or denied.
359
+ Raises CapabilityDeniedError if denied. Call once at startup, not on every render.
360
+
361
+ emit.cd_to(cwd)
362
+ Request the host to cd all terminals in the same pane group to cwd.
363
+
364
+ emit.run_get(intent, payload=None) -> str
365
+ Dispatch an intent-based AI/agent job. Returns a run_id. Progress arrives
366
+ via RunUpdate PlexiEvents; handle them in on_run_update if needed.
367
+
368
+ emit.pipe_open(pipe_id, mode="binary", direction="in") -> Pipe
369
+ Open a typed pipe and return a Pipe handle.
370
+ mode: "json" | "binary" direction: "in" | "out" | "duplex"
371
+ For binary mode, call pipe.connect() and wait for PipeOpened before I/O.
372
+
373
+
374
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
375
+ STRUCTURED ARGUMENT SHAPES
376
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
377
+
378
+ mods (passed to on_key):
379
+ {"shift": bool, "ctrl": bool, "alt": bool, "meta": bool}
380
+
381
+ ListItem (each element of the items list passed to ctx.list_view):
382
+ {
383
+ "title": str, # primary label (required)
384
+ "subtitle": str, # secondary label (optional)
385
+ "icon": str, # SF Symbol name or emoji (optional)
386
+ "color": str, # override title color (optional hex)
387
+ "tag": str, # right-aligned badge text (optional)
388
+ }
389
+
390
+ NotifyAction (each element of the actions list passed to notify):
391
+ {
392
+ "label": str, # button label shown in the notification
393
+ "key": str, # identifier sent back in a Command event
394
+ }
395
+
396
+ Pipe (returned by emit.pipe_open):
397
+ pipe.connect(timeout=5.0) -> bool — wait for the socket to be ready
398
+ pipe.read_frame() -> bytes | None — read one length-prefixed frame
399
+ pipe.write_frame(data) — write one length-prefixed frame
400
+ pipe.send(payload) — JSON-mode send (dict/list/scalar)
401
+ pipe.close() — release the socket
402
+
403
+
404
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
405
+ App EVENT HANDLERS (override in your subclass)
406
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
407
+
408
+ on_init(self, ctx) — after Init handshake completes
409
+ on_render(self, ctx) — on each Render event; auto-sends FrameDone
410
+ on_key(self, ctx, key, mods) — on Key event
411
+ on_click(self, ctx, x, y, button) — on Click event
412
+ on_command(self, ctx, text) — on Command event (command palette)
413
+ on_pipe_message(self, ctx, pipe_id, payload) — on PipeMessage (json-mode pipe)
414
+ on_path_changed(self, ctx, cwd) — on PathChanged broadcast
415
+ on_inject(self, ctx, payload) — on InjectState from the host
416
+ on_suspend(self) — on Suspend (app hidden/backgrounded)
417
+ on_resume(self) — on Resume (app visible again)
418
+ on_shutdown(self) — on Shutdown (clean up before exit)
419
+
420
+ All handlers except on_suspend, on_resume, and on_shutdown receive a
421
+ RenderContext as their first argument. on_render is the only handler that
422
+ auto-emits FrameDone; all others must NOT emit FrameDone.
423
+
424
+ ASYNC HANDLERS AND BLOCKING I/O (#393)
425
+
426
+ Input-driven hooks (on_key, on_click, on_command, on_paste, on_pipe_message,
427
+ on_path_changed, on_inject, on_timer) are dispatched as asyncio tasks — the
428
+ event loop does NOT wait for them to finish before processing the next event.
429
+ This means a slow handler never stalls the stdin reader or delays a Render.
430
+
431
+ Rules:
432
+
433
+ 1. Declare handlers ``async def`` whenever they need to do any I/O or call
434
+ ``await``-able Emitter helpers:
435
+
436
+ async def on_key(self, ctx, key, mods):
437
+ result = await self.emit.http_get(url) # non-blocking — fine
438
+
439
+ 2. Never call blocking operations directly from a handler. These freeze the
440
+ event loop thread and starve all other tasks:
441
+
442
+ def on_key(self, ctx, key, mods):
443
+ time.sleep(1) # BAD — blocks event loop thread
444
+ requests.get(url) # BAD — blocks event loop thread
445
+
446
+ Instead, use ``asyncio.to_thread`` from an async handler, or kick off a
447
+ ``threading.Thread`` and bridge back with ``emit.run_sync()``.
448
+
449
+ 3. ``on_render`` is awaited directly — all draw commands must complete before
450
+ FrameDone is sent. Keep on_render free of I/O; use on_render to read state
451
+ that background tasks have already fetched and stored.
452
+
453
+ 4. ``on_init`` and ``on_shutdown`` are also awaited (startup / teardown
454
+ ordering). Blocking I/O in on_init should use ``await`` Emitter helpers.
455
+
456
+ Call MyApp().run() to start the PGAP event loop. This blocks until Shutdown.
457
+ """
458
+
459
+ __version__ = "0.5.0"
460
+ SDK_ID = f"plexi-sdk-py/{__version__}"
461
+
462
+ from ._constants import (
463
+ TITLE, HEADING, BODY, CAPTION, HINT, MONO_BODY, MONO_SMALL,
464
+ PAD, PAD_TIGHT, HEADER_H, STATUS_H,
465
+ BG, SURFACE, HIGHLIGHT, ACCENT, MUTED, FG, RED, GREEN, YELLOW,
466
+ PRIORITY_LOW, PRIORITY_NORMAL, PRIORITY_HIGH, PRIORITY_CRITICAL,
467
+ rgba, dim,
468
+ )
469
+ from ._types import (
470
+ CapabilityDeniedError, VideoHandle,
471
+ RectCommand, TextCommand, BadgeCommand, TextInputSpec, ShortcutPair, NotifyOption,
472
+ )
473
+ from ._protocol import AiResponse, MidiPortInfo, MidiDeviceList, AudioDeviceInfo, AudioDeviceList, PROTOCOL_VERSION
474
+ from ._emitter import Emitter, _emit, _make_async_queue, _LOCK
475
+ from ._pipe import Pipe
476
+ from ._render_context import RenderContext, COMPACT_DEFAULT, REGULAR_DEFAULT
477
+ from ._app import App