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 +477 -0
- plexi_sdk/_app.py +1077 -0
- plexi_sdk/_constants.py +52 -0
- plexi_sdk/_emitter.py +1466 -0
- plexi_sdk/_pipe.py +92 -0
- plexi_sdk/_protocol.py +48 -0
- plexi_sdk/_render_context.py +976 -0
- plexi_sdk/_types.py +139 -0
- plexi_sdk/midi.py +222 -0
- plexi_sdk/py.typed +1 -0
- plexi_sdk/templates/__init__.py +0 -0
- plexi_sdk/templates/app_init.py +72 -0
- plexi_sdk/testing.py +451 -0
- plexi_sdk/ui.py +1535 -0
- plexi_sdk/widgets/__init__.py +27 -0
- plexi_sdk/widgets/button.py +60 -0
- plexi_sdk/widgets/keymap.py +51 -0
- plexi_sdk/widgets/list_view.py +159 -0
- plexi_sdk/widgets/scroll.py +100 -0
- plexi_sdk/widgets/text_area.py +218 -0
- plexi_sdk/widgets/text_buffer.py +337 -0
- plexi_sdk/widgets/text_input.py +70 -0
- plexi_sdk-0.4.0.dist-info/METADATA +127 -0
- plexi_sdk-0.4.0.dist-info/RECORD +25 -0
- plexi_sdk-0.4.0.dist-info/WHEEL +4 -0
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
|