gpui-bridge 0.0.0-stage → 0.3.0

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.
package/dist/app.js ADDED
@@ -0,0 +1,727 @@
1
+ // Runtime-agnostic client core: the App/UINode API and the handshake. It only
2
+ // needs a transport to the host process, so any JavaScript runtime can use it
3
+ // (Node: index.ts, txiki.js: txiki.ts).
4
+ import { PROTOCOL_VERSION, } from "./protocol.js";
5
+ import { Tree } from "./tree.js";
6
+ const HANDSHAKE_TIMEOUT_MS = 10_000;
7
+ export class HostExitedError extends Error {
8
+ exit;
9
+ constructor(message, exit) {
10
+ super(message);
11
+ this.exit = exit;
12
+ this.name = "HostExitedError";
13
+ }
14
+ }
15
+ function describeExit(exit, hostPath) {
16
+ if (exit.spawnError) {
17
+ const code = exit.spawnError.code;
18
+ return code === "ENOENT"
19
+ ? `gpui-bridge host not found at ${hostPath}. Build it with \`cargo build\` in host/ or pass hostPath.`
20
+ : `gpui-bridge host failed to start: ${exit.spawnError.message}`;
21
+ }
22
+ const how = exit.signal ? `signal ${exit.signal}` : `code ${exit.code}`;
23
+ const tail = exit.stderrTail.length ? `\nLast host output:\n ${exit.stderrTail.join("\n ")}` : "";
24
+ return `gpui-bridge host exited (${how}).${tail}`;
25
+ }
26
+ export class UINode {
27
+ app;
28
+ id;
29
+ type;
30
+ /** @internal */
31
+ constructor(app, id, type) {
32
+ this.app = app;
33
+ this.id = id;
34
+ this.type = type;
35
+ }
36
+ append(...children) {
37
+ for (const child of children) {
38
+ this.app.wire.queue({ op: "append", parent: this.id, child: child.id });
39
+ this.app.wire.attach(this.id, child.id);
40
+ }
41
+ return this;
42
+ }
43
+ insertBefore(child, before) {
44
+ this.app.wire.queue({ op: "insertBefore", parent: this.id, child: child.id, before: before.id });
45
+ this.app.wire.attach(this.id, child.id);
46
+ return this;
47
+ }
48
+ /** Merges props; `null` removes one. */
49
+ set(props) {
50
+ this.app.wire.queue({ op: "setProps", id: this.id, props });
51
+ return this;
52
+ }
53
+ /** Subscribes to an event; the host only emits subscribed events. */
54
+ on(event, handler) {
55
+ this.app.wire.setHandler(this.id, event, handler);
56
+ return this;
57
+ }
58
+ off(event) {
59
+ this.app.wire.setHandler(this.id, event, undefined);
60
+ return this;
61
+ }
62
+ /** Gives the node the keyboard focus on the next commit (an Input, a Textarea, a Command, a div with `keyDown`…). */
63
+ focus() {
64
+ this.app.wire.queue({ op: "focus", id: this.id });
65
+ return this;
66
+ }
67
+ /** Removes the node and all its descendants. */
68
+ remove() {
69
+ this.app.wire.remove(this.id);
70
+ }
71
+ }
72
+ /** A window other than the main one (`App.createWindow`). */
73
+ export class AppWindow {
74
+ app;
75
+ id;
76
+ closedListeners = new Set();
77
+ eventListeners = new Map();
78
+ /** The subscription list last sent, to avoid one message per listener. */
79
+ eventNames = "";
80
+ /** @internal */
81
+ constructor(app, id) {
82
+ this.app = app;
83
+ this.id = id;
84
+ }
85
+ /** Updates the window; like `App.window`, it opens on the first commit that sets `root`. */
86
+ set(options) {
87
+ this.app.queueWindow(this.id, options);
88
+ return this;
89
+ }
90
+ /** Closes the window on the next commit. No `closed` event follows: that is only for the user closing it. */
91
+ close() {
92
+ this.app.queueCloseWindow(this.id);
93
+ }
94
+ on(event, listener) {
95
+ if (event === "closed") {
96
+ this.closedListeners.add(listener);
97
+ return this;
98
+ }
99
+ const listeners = this.eventListeners.get(event) ?? new Set();
100
+ listeners.add(listener);
101
+ this.eventListeners.set(event, listeners);
102
+ this.syncEvents();
103
+ return this;
104
+ }
105
+ off(event, listener) {
106
+ if (event === "closed") {
107
+ this.closedListeners.delete(listener);
108
+ return this;
109
+ }
110
+ const listeners = this.eventListeners.get(event);
111
+ listeners?.delete(listener);
112
+ if (listeners?.size === 0)
113
+ this.eventListeners.delete(event);
114
+ this.syncEvents();
115
+ return this;
116
+ }
117
+ /** @internal */
118
+ handleClosed() {
119
+ for (const listener of this.closedListeners)
120
+ listener();
121
+ }
122
+ /** @internal */
123
+ handleWindowEvent(payload) {
124
+ for (const listener of this.eventListeners.get(payload.event) ?? [])
125
+ listener(payload);
126
+ }
127
+ /** Sends the subscription list only when the set of subscribed events changes. */
128
+ syncEvents() {
129
+ const events = [...this.eventListeners.keys()];
130
+ const names = events.join(",");
131
+ if (names === this.eventNames)
132
+ return;
133
+ this.eventNames = names;
134
+ this.app.queueWindow(this.id, { events });
135
+ }
136
+ }
137
+ export class App {
138
+ host;
139
+ hostPath;
140
+ /** @internal Protocol operations queued until `commit`. */
141
+ wire = new Tree();
142
+ listeners = {
143
+ error: new Set(),
144
+ windowClosed: new Set(),
145
+ windowEvent: new Set(),
146
+ shortcut: new Set(),
147
+ notification: new Set(),
148
+ tray: new Set(),
149
+ exit: new Set(),
150
+ };
151
+ exitInfo;
152
+ closing = false;
153
+ nextWindowId = 2;
154
+ windows = new Map();
155
+ /** Windows given a root and not closed: the host exits when the user closes the last one. */
156
+ openWindows = new Set();
157
+ nextRequest = 1;
158
+ /** System actions waiting for their `result`. */
159
+ requests = new Map();
160
+ /** A tray keeps the host running after the last window closes. */
161
+ hasTray = false;
162
+ exitWaiters = [];
163
+ /** @internal */
164
+ constructor(host, hostPath) {
165
+ this.host = host;
166
+ this.hostPath = hostPath;
167
+ }
168
+ div(props = {}) {
169
+ return this.create("div", props);
170
+ }
171
+ text(props = {}) {
172
+ return this.create("text", props);
173
+ }
174
+ button(props = {}) {
175
+ return this.create("Button", props);
176
+ }
177
+ input(props = {}) {
178
+ return this.create("Input", props);
179
+ }
180
+ checkbox(props = {}) {
181
+ return this.create("Checkbox", props);
182
+ }
183
+ icon(props = {}) {
184
+ return this.create("Icon", props);
185
+ }
186
+ image(props = {}) {
187
+ return this.create("Image", props);
188
+ }
189
+ switch(props = {}) {
190
+ return this.create("Switch", props);
191
+ }
192
+ radioGroup(props = {}) {
193
+ return this.create("RadioGroup", props);
194
+ }
195
+ select(props = {}) {
196
+ return this.create("Select", props);
197
+ }
198
+ slider(props = {}) {
199
+ return this.create("Slider", props);
200
+ }
201
+ textarea(props = {}) {
202
+ return this.create("Textarea", props);
203
+ }
204
+ /** A modal dialog shown while `open`; append its body as children. */
205
+ dialog(props = {}) {
206
+ return this.create("Dialog", props);
207
+ }
208
+ /** A list that builds only the rows in view; append one child per row (PROTOCOL.md section 15). */
209
+ virtualList(props = {}) {
210
+ return this.create("VirtualList", props);
211
+ }
212
+ /** A data table with text cells; it virtualizes its rows. */
213
+ table(props = {}) {
214
+ return this.create("Table", props);
215
+ }
216
+ /** A tab bar; show the chosen tab's content yourself. */
217
+ tabs(props = {}) {
218
+ return this.create("Tabs", props);
219
+ }
220
+ /** One tab of a `Tabs`; append the nodes of its chip (an `Icon`, a `text`, a `Tag`…; a last `Button` is drawn after them). */
221
+ tab(props = {}) {
222
+ return this.create("Tab", props);
223
+ }
224
+ /** A menu bar: one button per menu (PROTOCOL.md section 10). */
225
+ menuBar(props = {}) {
226
+ return this.create("MenuBar", props);
227
+ }
228
+ /** An inline message; `close` when it is `closable` (PROTOCOL.md section 17). */
229
+ alert(props = {}) {
230
+ return this.create("Alert", props);
231
+ }
232
+ /** A count, a dot or an icon over its children. */
233
+ badge(props = {}) {
234
+ return this.create("Badge", props);
235
+ }
236
+ tag(props = {}) {
237
+ return this.create("Tag", props);
238
+ }
239
+ /** A key or shortcut, such as `ctrl-s`. */
240
+ kbd(props = {}) {
241
+ return this.create("Kbd", props);
242
+ }
243
+ /** A progress bar: `value` from 0 to 100. */
244
+ progress(props = {}) {
245
+ return this.create("Progress", props);
246
+ }
247
+ /** A spinner; give it a size with `class` (`size-4`). */
248
+ spinner(props = {}) {
249
+ return this.create("Spinner", props);
250
+ }
251
+ /** A loading placeholder. */
252
+ skeleton(props = {}) {
253
+ return this.create("Skeleton", props);
254
+ }
255
+ /** Stars: `change` with the chosen count. */
256
+ rating(props = {}) {
257
+ return this.create("Rating", props);
258
+ }
259
+ /** Page navigation: `change` with the chosen page (1-based). */
260
+ pagination(props = {}) {
261
+ return this.create("Pagination", props);
262
+ }
263
+ /** Content shown while `open`; toggle it with anything that takes a click. */
264
+ collapsible(props = {}) {
265
+ return this.create("Collapsible", props);
266
+ }
267
+ /** Sections as `items`; `change` with the id of the one toggled. */
268
+ accordion(props = {}) {
269
+ return this.create("Accordion", props);
270
+ }
271
+ /** A user picture, or their initial. */
272
+ avatar(props = {}) {
273
+ return this.create("Avatar", props);
274
+ }
275
+ /** A step indicator: `change` with the chosen step's `value`. */
276
+ stepper(props = {}) {
277
+ return this.create("Stepper", props);
278
+ }
279
+ /** A text field with increment and decrement buttons. */
280
+ numberInput(props = {}) {
281
+ return this.create("NumberInput", props);
282
+ }
283
+ /** A one-time code, one cell per digit. */
284
+ otpInput(props = {}) {
285
+ return this.create("OtpInput", props);
286
+ }
287
+ /** A form; append `Field`s to it. */
288
+ form(props = {}) {
289
+ return this.create("Form", props);
290
+ }
291
+ /** A label, an optional description and its children as the control. */
292
+ field(props = {}) {
293
+ return this.create("Field", props);
294
+ }
295
+ /** A titled box; append its content. */
296
+ groupBox(props = {}) {
297
+ return this.create("GroupBox", props);
298
+ }
299
+ /** Key-value rows. */
300
+ descriptionList(props = {}) {
301
+ return this.create("DescriptionList", props);
302
+ }
303
+ /** A status bar with text on the left and on the right. */
304
+ statusBar(props = {}) {
305
+ return this.create("StatusBar", props);
306
+ }
307
+ /** A toolbar; append its content. */
308
+ toolbar(props = {}) {
309
+ return this.create("Toolbar", props);
310
+ }
311
+ /** Panels the user can resize; append one child per panel. */
312
+ resizable(props = {}) {
313
+ return this.create("Resizable", props);
314
+ }
315
+ /** A month view: `change` with the chosen day (`YYYY-MM-DD`). */
316
+ calendar(props = {}) {
317
+ return this.create("Calendar", props);
318
+ }
319
+ /** Items in props; `change` with the chosen item's id, `expand`/`collapse` with a folder's id. */
320
+ tree(props = {}) {
321
+ return this.create("Tree", props);
322
+ }
323
+ /** A parsed patch; `change` with the selected lines. */
324
+ diff(props = {}) {
325
+ return this.create("Diff", props);
326
+ }
327
+ /** Slides as children; `change` with the selected index. */
328
+ carousel(props = {}) {
329
+ return this.create("Carousel", props);
330
+ }
331
+ /** Sections of entries in props; `change` with the clicked entry's id. */
332
+ sidebar(props = {}) {
333
+ return this.create("Sidebar", props);
334
+ }
335
+ /** A line, bar or pie chart of the points in `data`. */
336
+ chart(props = {}) {
337
+ return this.create("Chart", props);
338
+ }
339
+ /** Paints the drawing commands in `commands`; `pointerDown` with the local point. */
340
+ canvas(props = {}) {
341
+ return this.create("Canvas", props);
342
+ }
343
+ /** A command palette over the sections in `items`; `confirm` with the chosen entry's `id`. */
344
+ command(props = {}) {
345
+ return this.create("Command", props);
346
+ }
347
+ /** Settings pages of typed fields in `pages`; `change` with `{key, value}` of the field set. */
348
+ settings(props = {}) {
349
+ return this.create("Settings", props);
350
+ }
351
+ /** A button that stays pressed: `change` with `checked`. */
352
+ toggle(props = {}) {
353
+ return this.create("Toggle", props);
354
+ }
355
+ /** An action with a menu: `click` for the left half, `menu` with the chosen item's id. */
356
+ dropdownButton(props = {}) {
357
+ return this.create("DropdownButton", props);
358
+ }
359
+ /** Text with an optional secondary text and a highlighted substring. */
360
+ label(props = {}) {
361
+ return this.create("Label", props);
362
+ }
363
+ /** A centered empty state; append the action (usually a `Button`). */
364
+ empty(props = {}) {
365
+ return this.create("Empty", props);
366
+ }
367
+ /** Loading text with a moving highlight. */
368
+ shimmer(props = {}) {
369
+ return this.create("Shimmer", props);
370
+ }
371
+ /** A text field with text and an icon on either side: `change` and `submit` like an `Input`. */
372
+ inputGroup(props = {}) {
373
+ return this.create("InputGroup", props);
374
+ }
375
+ /** Copies `value` on click: `copied` with that text. */
376
+ clipboard(props = {}) {
377
+ return this.create("Clipboard", props);
378
+ }
379
+ /** A confirmation with a title, a description and an icon: `ok` and `cancel`, as in a `Dialog`. */
380
+ alertDialog(props = {}) {
381
+ return this.create("AlertDialog", props);
382
+ }
383
+ /** A panel that slides in from `side`: `close` with its X, Escape or a click on the overlay. */
384
+ sheet(props = {}) {
385
+ return this.create("Sheet", props);
386
+ }
387
+ /** A panel anchored to its first child (the trigger): `change` with `checked` both ways. */
388
+ popover(props = {}) {
389
+ return this.create("Popover", props);
390
+ }
391
+ /** A card on hover, opened by the mouse alone: `change` with `checked`. */
392
+ hoverCard(props = {}) {
393
+ return this.create("HoverCard", props);
394
+ }
395
+ /** A day picker: `change` with the chosen day (`null` when cleared). */
396
+ datePicker(props = {}) {
397
+ return this.create("DatePicker", props);
398
+ }
399
+ /** A time typed by segments: `change` with the time. */
400
+ timeField(props = {}) {
401
+ return this.create("TimeField", props);
402
+ }
403
+ /** A color swatch and hex box: `change` with the color (`null` when cleared). */
404
+ colorPicker(props = {}) {
405
+ return this.create("ColorPicker", props);
406
+ }
407
+ /** A list that picks several options: `change` with the whole selection. */
408
+ combobox(props = {}) {
409
+ return this.create("Combobox", props);
410
+ }
411
+ /** A chat bubble; its children are the content. */
412
+ bubble(props = {}) {
413
+ return this.create("Bubble", props);
414
+ }
415
+ /** A message row: `MessageHeader`/`MessageFooter` children, the rest is the content. */
416
+ message(props = {}) {
417
+ return this.create("Message", props);
418
+ }
419
+ /** A `Message`'s header (sender, time). */
420
+ messageHeader(props = {}) {
421
+ return this.create("MessageHeader", props);
422
+ }
423
+ /** A `Message`'s footer (state, actions). */
424
+ messageFooter(props = {}) {
425
+ return this.create("MessageFooter", props);
426
+ }
427
+ /** A compact status row in a conversation. */
428
+ marker(props = {}) {
429
+ return this.create("Marker", props);
430
+ }
431
+ /** A file or picture card: `click`, `remove` and `retry` show its controls. */
432
+ attachment(props = {}) {
433
+ return this.create("Attachment", props);
434
+ }
435
+ /** A virtual list of messages anchored at the end; each child is a row. */
436
+ messageScroller(props = {}) {
437
+ return this.create("MessageScroller", props);
438
+ }
439
+ /** Questions one at a time: `change` with each answer, `submit` with all of them. */
440
+ questionnaire(props = {}) {
441
+ return this.create("Questionnaire", props);
442
+ }
443
+ /** A selectable list over its `ListItem` children: `change`, `confirm`, `query` and `loadMore`. */
444
+ list(props = {}) {
445
+ return this.create("List", props);
446
+ }
447
+ /** A `List` row: `value` is what the events report; its children are the content. */
448
+ listItem(props = {}) {
449
+ return this.create("ListItem", props);
450
+ }
451
+ /** Panels the user rearranges: its `DockPanel` children. The host owns the layout. */
452
+ dock(props = {}) {
453
+ return this.create("Dock", props);
454
+ }
455
+ /** A `Dock` panel: `title`, `placement` (read once) and `closable` (`close`); its children are the content. */
456
+ dockPanel(props = {}) {
457
+ return this.create("DockPanel", props);
458
+ }
459
+ /** An `Accordion` section: `id` (what `change` reports), `title`, `open`, `disabled`; its children are the content. */
460
+ accordionItem(props = {}) {
461
+ return this.create("AccordionItem", props);
462
+ }
463
+ /** A `Dialog`'s or `AlertDialog`'s footer: its children (buttons) replace OK/Cancel. */
464
+ dialogFooter(props = {}) {
465
+ return this.create("DialogFooter", props);
466
+ }
467
+ /** A `StatusBar`'s left side: its children replace the `left` text. */
468
+ statusBarLeft(props = {}) {
469
+ return this.create("StatusBarLeft", props);
470
+ }
471
+ /** A `StatusBar`'s right side: its children replace the `right` text. */
472
+ statusBarRight(props = {}) {
473
+ return this.create("StatusBarRight", props);
474
+ }
475
+ /** A node of any type; the specific methods below only add typing. */
476
+ create(type, props = {}) {
477
+ return new UINode(this, this.wire.create(type, { ...props }), type);
478
+ }
479
+ /** Creates or updates the main window (window 1). It opens on the first commit that sets `root`. */
480
+ window(options) {
481
+ this.queueWindow(undefined, options);
482
+ return this;
483
+ }
484
+ /** Another window, with its own root; it opens on the first commit that sets `root`. */
485
+ createWindow(options = {}) {
486
+ const window = new AppWindow(this, this.nextWindowId++);
487
+ this.windows.set(window.id, window);
488
+ return window.set(options);
489
+ }
490
+ /** @internal */
491
+ queueWindow(id, options) {
492
+ const { root, ...rest } = options;
493
+ this.wire.queue({ op: "window", ...(id !== undefined && { id }), ...rest, ...(root && { root: root.id }) });
494
+ if (root)
495
+ this.openWindows.add(id ?? 1);
496
+ }
497
+ /** @internal */
498
+ queueCloseWindow(id) {
499
+ this.wire.queue({ op: "closeWindow", id });
500
+ this.openWindows.delete(id);
501
+ }
502
+ /** Native "open" dialog; resolves with the chosen paths, or `null` if cancelled. */
503
+ async openDialog(options = {}) {
504
+ return (await this.request({ op: "openDialog", request: 0, ...options })).paths ?? null;
505
+ }
506
+ /** Native "save" dialog; resolves with the chosen path, or `null` if cancelled. */
507
+ async saveDialog(options = {}) {
508
+ return (await this.request({ op: "saveDialog", request: 0, ...options })).path ?? null;
509
+ }
510
+ /** The clipboard's text, or `null` if it holds none. */
511
+ async readClipboard() {
512
+ return (await this.request({ op: "readClipboard", request: 0 })).text ?? null;
513
+ }
514
+ /** Shows a toast now (not on commit). */
515
+ notify(options) {
516
+ this.action({ op: "notify", ...options });
517
+ }
518
+ writeClipboard(text) {
519
+ this.action({ op: "writeClipboard", text });
520
+ }
521
+ /** Opens a URL with the system's handler (the browser for http). */
522
+ openUrl(url) {
523
+ this.action({ op: "openUrl", url });
524
+ }
525
+ /** Opens a file or folder with its default app. */
526
+ openPath(path) {
527
+ this.action({ op: "openPath", path });
528
+ }
529
+ /** Actions run when the host gets them, not on commit. */
530
+ action(message) {
531
+ if (this.exitInfo)
532
+ throw new HostExitedError(describeExit(this.exitInfo, this.hostPath), this.exitInfo);
533
+ this.host.send(message);
534
+ }
535
+ request(message) {
536
+ const request = this.nextRequest++;
537
+ return new Promise((resolve, reject) => {
538
+ this.action({ ...message, request });
539
+ this.requests.set(request, { resolve, reject });
540
+ });
541
+ }
542
+ /** App-wide keyboard shortcuts (`ctrl-s`, `f5`…), applied on the next commit; replaces the list. */
543
+ shortcuts(keys) {
544
+ this.wire.queue({ op: "shortcuts", keys });
545
+ return this;
546
+ }
547
+ /** Shows or updates the system tray icon on the next commit. */
548
+ tray(options) {
549
+ this.wire.queue({ op: "tray", ...options });
550
+ this.hasTray = true;
551
+ return this;
552
+ }
553
+ /** Removes the tray icon on the next commit; with no window open, the host then exits. */
554
+ removeTray() {
555
+ this.wire.queue({ op: "removeTray" });
556
+ this.hasTray = false;
557
+ if (this.openWindows.size === 0)
558
+ this.closing = true;
559
+ return this;
560
+ }
561
+ /** Sends every pending operation; the host renders once per commit. */
562
+ commit() {
563
+ if (this.exitInfo)
564
+ throw new HostExitedError(describeExit(this.exitInfo, this.hostPath), this.exitInfo);
565
+ this.wire.commit((message) => this.host.send(message));
566
+ }
567
+ on(event, listener) {
568
+ this.listeners[event].add(listener);
569
+ return this;
570
+ }
571
+ off(event, listener) {
572
+ this.listeners[event].delete(listener);
573
+ return this;
574
+ }
575
+ /** Closes the connection; resolves when the host has exited. */
576
+ close() {
577
+ this.closing = true;
578
+ this.host.close();
579
+ return this.exited();
580
+ }
581
+ /** Resolves when the host process has exited, for any reason. */
582
+ exited() {
583
+ if (this.exitInfo)
584
+ return Promise.resolve(this.exitInfo);
585
+ return new Promise((resolve) => this.exitWaiters.push(resolve));
586
+ }
587
+ /** @internal */
588
+ handleMessage(message) {
589
+ switch (message.type) {
590
+ case "event": {
591
+ const handler = this.wire.handler(message.id, message.event);
592
+ if (!handler)
593
+ return;
594
+ const type = this.wire.type(message.id);
595
+ const payload = message.event === "menu"
596
+ ? { item: message.value ?? "" }
597
+ : message.event === "keyDown"
598
+ ? { key: message.value ?? "" }
599
+ : message.event === "drop"
600
+ ? { paths: message.paths ?? [] }
601
+ : message.event === "sort"
602
+ ? message.value
603
+ : type === "Checkbox" ||
604
+ type === "Switch" ||
605
+ type === "Toggle" ||
606
+ type === "Popover" ||
607
+ type === "HoverCard"
608
+ ? { checked: message.checked ?? false }
609
+ : message.value !== undefined
610
+ ? { value: message.value }
611
+ : type === "Input" || type === "Textarea"
612
+ ? { value: "" }
613
+ : {};
614
+ handler(payload);
615
+ return;
616
+ }
617
+ case "error": {
618
+ const error = { code: message.code, message: message.message, op: message.op };
619
+ // A rejected system action: its promise fails instead of waiting for a result.
620
+ const pending = message.request !== undefined ? this.requests.get(message.request) : undefined;
621
+ if (pending) {
622
+ this.requests.delete(message.request);
623
+ pending.reject(Object.assign(new Error(`gpui-bridge: host rejected '${error.op}': ${error.message}`), { host: error }));
624
+ return;
625
+ }
626
+ if (!this.emit("error", error))
627
+ console.warn(`gpui-bridge: host rejected '${error.op}': ${error.message}`);
628
+ return;
629
+ }
630
+ case "windowClosed":
631
+ this.openWindows.delete(message.id);
632
+ // The host exits after the last window, unless there is a tray.
633
+ if (this.openWindows.size === 0 && !this.hasTray)
634
+ this.closing = true;
635
+ this.windows.get(message.id)?.handleClosed();
636
+ this.emit("windowClosed", { id: message.id });
637
+ return;
638
+ case "windowEvent": {
639
+ const payload = {
640
+ id: message.id,
641
+ event: message.event,
642
+ x: message.x,
643
+ y: message.y,
644
+ width: message.width,
645
+ height: message.height,
646
+ visible: message.visible,
647
+ };
648
+ this.windows.get(message.id)?.handleWindowEvent(payload);
649
+ this.emit("windowEvent", payload);
650
+ return;
651
+ }
652
+ case "result":
653
+ this.requests.get(message.request)?.resolve(message);
654
+ this.requests.delete(message.request);
655
+ return;
656
+ case "notification":
657
+ this.emit("notification", { id: message.id, window: message.window });
658
+ return;
659
+ case "shortcut":
660
+ this.emit("shortcut", { keys: message.keys, window: message.window });
661
+ return;
662
+ case "tray":
663
+ this.emit("tray", message.event === "menu" ? { event: "menu", item: message.value ?? "" } : { event: "click" });
664
+ return;
665
+ case "ready":
666
+ return;
667
+ }
668
+ }
669
+ /** @internal */
670
+ handleExit(exit) {
671
+ this.exitInfo = exit;
672
+ // An exit after close() or windowClosed is expected; anything else is a crash.
673
+ const unexpected = !this.closing;
674
+ if (!this.emit("exit", exit) && unexpected)
675
+ console.error(describeExit(exit, this.hostPath));
676
+ for (const resolve of this.exitWaiters.splice(0))
677
+ resolve(exit);
678
+ for (const { reject } of this.requests.values())
679
+ reject(new HostExitedError(describeExit(exit, this.hostPath), exit));
680
+ this.requests.clear();
681
+ }
682
+ emit(event, payload) {
683
+ for (const listener of this.listeners[event])
684
+ listener(payload);
685
+ return this.listeners[event].size > 0;
686
+ }
687
+ }
688
+ /**
689
+ * Starts the host through `start` and completes the handshake. Rejects with a
690
+ * clear message if the host cannot start.
691
+ */
692
+ export function connect(hostPath, start) {
693
+ return new Promise((resolve, reject) => {
694
+ let app;
695
+ let ready = false;
696
+ const timer = setTimeout(() => {
697
+ host.kill();
698
+ reject(new Error(`gpui-bridge host did not answer the handshake within ${HANDSHAKE_TIMEOUT_MS} ms`));
699
+ }, HANDSHAKE_TIMEOUT_MS);
700
+ const host = start({
701
+ onMessage(message) {
702
+ if (ready)
703
+ return app.handleMessage(message);
704
+ if (message.type === "ready") {
705
+ ready = true;
706
+ clearTimeout(timer);
707
+ resolve(app);
708
+ }
709
+ else if (message.type === "error") {
710
+ clearTimeout(timer);
711
+ reject(new Error(`gpui-bridge handshake failed (${message.code}): ${message.message}`));
712
+ }
713
+ },
714
+ onCorruptLine(line) {
715
+ console.error(`gpui-bridge: host wrote a non-protocol line to stdout: ${line}`);
716
+ },
717
+ onExit(exit) {
718
+ if (ready)
719
+ return app.handleExit(exit);
720
+ clearTimeout(timer);
721
+ reject(new HostExitedError(describeExit(exit, hostPath), exit));
722
+ },
723
+ });
724
+ app = new App(host, hostPath);
725
+ host.send({ op: "hello", protocol: PROTOCOL_VERSION });
726
+ });
727
+ }