@bun-native/protocol 0.0.0-stage → 0.33.1

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.
@@ -0,0 +1,1490 @@
1
+ import {
2
+ parseAppMenuItemId,
3
+ parseAppMenus,
4
+ parseContextMenuItems,
5
+ type AppMenu,
6
+ type AppMenuEntry,
7
+ } from './app-menu.ts'
8
+ import {
9
+ parseFileDialogOptions,
10
+ parseFileDialogResult,
11
+ type FileDialogOptions,
12
+ type FileDialogResult,
13
+ } from './dialogs.ts'
14
+ import { parseElement, type Element } from './elements.ts'
15
+ import {
16
+ parseMessageDialogOptions,
17
+ parseMessageDialogResult,
18
+ type MessageDialogOptions,
19
+ type MessageDialogResult,
20
+ } from './message-dialogs.ts'
21
+ import { MAX_FRAME_BYTES, ProtocolError } from './framing.ts'
22
+ import {
23
+ parseDisplaySnapshot,
24
+ parseClipboardText,
25
+ parseServicePolicy,
26
+ parseShortcutKey,
27
+ parseShortcutModifiers,
28
+ parseNotificationContent,
29
+ parseNotificationDismissal,
30
+ MAX_CLIPBOARD_BYTES,
31
+ MAX_GLOBAL_SHORTCUTS,
32
+ MAX_NOTIFICATIONS,
33
+ MAX_SERVICE_REQUESTS,
34
+ type ClipboardText,
35
+ type DisplaySnapshot,
36
+ type NotificationDismissal,
37
+ type ServicePolicy,
38
+ type ShortcutModifiers,
39
+ } from './services.ts'
40
+ import {
41
+ MAX_FILE_CHUNK_BYTES,
42
+ MAX_FILE_GRANTS,
43
+ parseFileAccess,
44
+ parseFileOperation,
45
+ parseFilePath,
46
+ parseFileResult,
47
+ parseRootId,
48
+ type FileAccess,
49
+ type FileOperation,
50
+ type FileResult,
51
+ } from './files.ts'
52
+ import {
53
+ MAX_PROCESSES,
54
+ parseProcessExit,
55
+ parseProcessLaunch,
56
+ parseProcessOutput,
57
+ parseProcessSignal,
58
+ parseProcessStream,
59
+ parseTerminalSize,
60
+ type ProcessExit,
61
+ type ProcessLaunch,
62
+ type ProcessSignal,
63
+ type ProcessStream,
64
+ type TerminalSize,
65
+ } from './processes.ts'
66
+ import {
67
+ MAX_DROP_ITEMS,
68
+ MAX_DROP_TARGETS,
69
+ parseDragState,
70
+ parseDropItemIndex,
71
+ parseDroppedItems,
72
+ parseDropRejection,
73
+ type DragState,
74
+ type DroppedItem,
75
+ type DropRejection,
76
+ } from './drops.ts'
77
+ import {
78
+ fields,
79
+ id,
80
+ integer,
81
+ keys,
82
+ MAX_TEXT_BYTES,
83
+ MAX_TITLE_BYTES,
84
+ number,
85
+ object,
86
+ string,
87
+ } from './values.ts'
88
+
89
+ export const PROTOCOL_VERSION = 12
90
+ /** Four operations per node of a full window: a whole-window replacement fits one batch. */
91
+ export const MAX_OPERATIONS = 16_384
92
+ const MAX_NODES_PER_WINDOW = 4_096
93
+ const MAX_WINDOWS = 16
94
+ const MAX_DEPTH = 64
95
+ const MAX_TEXT_BYTES_PER_WINDOW = 4_194_304
96
+
97
+ export interface Limits {
98
+ max_frame_bytes: number
99
+ max_operations: number
100
+ max_nodes_per_window: number
101
+ max_windows: number
102
+ max_depth: number
103
+ max_text_bytes: number
104
+ max_text_bytes_per_window: number
105
+ max_clipboard_bytes: number
106
+ max_service_requests: number
107
+ max_global_shortcuts: number
108
+ max_notifications: number
109
+ max_file_grants: number
110
+ max_file_chunk_bytes: number
111
+ max_processes: number
112
+ max_drop_targets: number
113
+ max_drop_items: number
114
+ }
115
+
116
+ export const DEFAULT_LIMITS: Readonly<Limits> = Object.freeze({
117
+ max_frame_bytes: MAX_FRAME_BYTES,
118
+ max_operations: MAX_OPERATIONS,
119
+ max_nodes_per_window: MAX_NODES_PER_WINDOW,
120
+ max_windows: MAX_WINDOWS,
121
+ max_depth: MAX_DEPTH,
122
+ max_text_bytes: MAX_TEXT_BYTES,
123
+ max_text_bytes_per_window: MAX_TEXT_BYTES_PER_WINDOW,
124
+ max_clipboard_bytes: MAX_CLIPBOARD_BYTES,
125
+ max_service_requests: MAX_SERVICE_REQUESTS,
126
+ max_global_shortcuts: MAX_GLOBAL_SHORTCUTS,
127
+ max_notifications: MAX_NOTIFICATIONS,
128
+ max_file_grants: MAX_FILE_GRANTS,
129
+ max_file_chunk_bytes: MAX_FILE_CHUNK_BYTES,
130
+ max_processes: MAX_PROCESSES,
131
+ max_drop_targets: MAX_DROP_TARGETS,
132
+ max_drop_items: MAX_DROP_ITEMS,
133
+ })
134
+
135
+ export type Operation =
136
+ | { type: 'create'; node_id: number; element: Element }
137
+ | {
138
+ type: 'set'
139
+ node_id: number
140
+ element: Exclude<Element, { kind: 'input' }>
141
+ }
142
+ | {
143
+ type: 'set'
144
+ node_id: number
145
+ element: Extract<Element, { kind: 'input' }>
146
+ expected_input_revision: number
147
+ }
148
+ | { type: 'insert'; parent_id: number; child_id: number; index: number }
149
+ | { type: 'delete'; node_id: number }
150
+ /** Requires `focus_v1`; applied after the batch's tree changes. */
151
+ | { type: 'focus'; node_id: number }
152
+ /** Requires `scroll_control_v1`; offsets from the content start, clamped natively. */
153
+ | { type: 'scroll_to'; node_id: number; x?: number; y?: number }
154
+ /** Requires `scroll_control_v1`. */
155
+ | { type: 'scroll_into_view'; node_id: number }
156
+
157
+ /** Largest programmatic scroll offset, in logical pixels. */
158
+ export const MAX_SCROLL_OFFSET = 1_000_000
159
+
160
+ export type Command =
161
+ | { id: number; type: 'hello'; protocol: typeof PROTOCOL_VERSION }
162
+ | {
163
+ id: number
164
+ type: 'open'
165
+ window_id: number
166
+ root_id: number
167
+ title: string
168
+ width: number
169
+ height: number
170
+ }
171
+ | {
172
+ id: number
173
+ type: 'batch'
174
+ window_id: number
175
+ base_revision: number
176
+ operations: readonly Operation[]
177
+ }
178
+ | { id: number; type: 'close'; window_id: number }
179
+ | { id: number; type: 'display_snapshot'; window_id: number }
180
+ | { id: number; type: 'clipboard_read'; window_id: number }
181
+ | { id: number; type: 'clipboard_write'; window_id: number; text: string }
182
+ | {
183
+ id: number
184
+ type: 'register_global_shortcut'
185
+ window_id: number
186
+ shortcut_id: number
187
+ key: string
188
+ modifiers: ShortcutModifiers
189
+ }
190
+ | {
191
+ id: number
192
+ type: 'unregister_global_shortcut'
193
+ window_id: number
194
+ shortcut_id: number
195
+ }
196
+ | {
197
+ id: number
198
+ type: 'post_notification'
199
+ window_id: number
200
+ notification_id: number
201
+ title: string
202
+ body: string
203
+ }
204
+ | {
205
+ id: number
206
+ type: 'withdraw_notification'
207
+ window_id: number
208
+ notification_id: number
209
+ }
210
+ | {
211
+ id: number
212
+ type: 'open_file_grant'
213
+ window_id: number
214
+ grant_id: number
215
+ root: string
216
+ access: FileAccess
217
+ }
218
+ | {
219
+ id: number
220
+ type: 'close_file_grant'
221
+ window_id: number
222
+ grant_id: number
223
+ }
224
+ | {
225
+ id: number
226
+ type: 'file_request'
227
+ window_id: number
228
+ grant_id: number
229
+ path: string
230
+ operation: FileOperation
231
+ }
232
+ | {
233
+ id: number
234
+ type: 'spawn_process'
235
+ window_id: number
236
+ process_id: number
237
+ launch: ProcessLaunch
238
+ }
239
+ | { id: number; type: 'write_process'; window_id: number; process_id: number; data: string }
240
+ | { id: number; type: 'close_process_input'; window_id: number; process_id: number }
241
+ | {
242
+ id: number
243
+ type: 'resize_process'
244
+ window_id: number
245
+ process_id: number
246
+ size: TerminalSize
247
+ }
248
+ | {
249
+ id: number
250
+ type: 'signal_process'
251
+ window_id: number
252
+ process_id: number
253
+ signal: ProcessSignal
254
+ }
255
+ | {
256
+ id: number
257
+ type: 'acknowledge_process_output'
258
+ window_id: number
259
+ process_id: number
260
+ chunk: number
261
+ }
262
+ | { id: number; type: 'release_process'; window_id: number; process_id: number }
263
+ | {
264
+ id: number
265
+ type: 'show_file_dialog'
266
+ window_id: number
267
+ dialog_id: number
268
+ options: FileDialogOptions
269
+ }
270
+ | {
271
+ id: number
272
+ type: 'close_file_dialog'
273
+ window_id: number
274
+ dialog_id: number
275
+ }
276
+ | {
277
+ id: number
278
+ type: 'show_message_dialog'
279
+ window_id: number
280
+ dialog_id: number
281
+ options: MessageDialogOptions
282
+ }
283
+ | {
284
+ id: number
285
+ type: 'close_message_dialog'
286
+ window_id: number
287
+ dialog_id: number
288
+ }
289
+ /** Shows an OS context menu at a logical point of the window's content. */
290
+ | {
291
+ id: number
292
+ type: 'show_context_menu'
293
+ window_id: number
294
+ menu_id: number
295
+ x: number
296
+ y: number
297
+ items: AppMenuEntry[]
298
+ }
299
+ | { id: number; type: 'close_context_menu'; window_id: number; menu_id: number }
300
+ | {
301
+ id: number
302
+ type: 'register_drop_target'
303
+ window_id: number
304
+ target_id: number
305
+ node_id: number
306
+ }
307
+ | { id: number; type: 'unregister_drop_target'; window_id: number; target_id: number }
308
+ | {
309
+ id: number
310
+ type: 'resolve_dropped_item'
311
+ window_id: number
312
+ target_id: number
313
+ drop_id: number
314
+ item: number
315
+ root: string
316
+ }
317
+ /** Replaces the application menu bar; an empty `menus` removes it. */
318
+ | { id: number; type: 'set_app_menu'; menu_id: number; menus: AppMenu[] }
319
+ | { id: number; type: 'shutdown' }
320
+
321
+ export const ERROR_CODES = [
322
+ 'invalid_command',
323
+ 'version_mismatch',
324
+ 'invalid_state',
325
+ 'invalid_handle',
326
+ 'stale_revision',
327
+ 'limit_exceeded',
328
+ 'native_failure',
329
+ 'permission_denied',
330
+ 'service_unavailable',
331
+ 'cancelled',
332
+ 'operation_timed_out',
333
+ 'payload_too_large',
334
+ 'conflict',
335
+ 'not_found',
336
+ 'already_exists',
337
+ 'path_denied',
338
+ 'wrong_kind',
339
+ 'not_empty',
340
+ 'access_denied',
341
+ ] as const
342
+ export type ErrorCode = (typeof ERROR_CODES)[number]
343
+ export const REQUIRED_CAPABILITIES = Object.freeze([
344
+ 'tree',
345
+ 'input_revisions',
346
+ 'layout_v1',
347
+ 'interactions_v1',
348
+ 'window_display_v1',
349
+ ] as const)
350
+ const CAPABILITIES = [
351
+ ...REQUIRED_CAPABILITIES,
352
+ 'clipboard_text_v1',
353
+ 'global_shortcuts_v1',
354
+ 'notifications_v1',
355
+ 'filesystem_grants_v1',
356
+ 'processes_v1',
357
+ 'split_v1',
358
+ 'style_decoration_v1',
359
+ 'display_details_v1',
360
+ 'display_changes_v1',
361
+ 'file_dialogs_v1',
362
+ 'overlays_v1',
363
+ 'scroll_anchor_v1',
364
+ 'focus_v1',
365
+ 'scroll_control_v1',
366
+ 'overlay_keyboard_v1',
367
+ 'popup_windows_v1',
368
+ 'drag_drop_files_v1',
369
+ 'input_states_v1',
370
+ 'app_menu_v1',
371
+ 'task_dialogs_v1',
372
+ 'context_menus_v1',
373
+ 'split_resize_v1',
374
+ 'accessibility_v1',
375
+ 'accessibility_lists_v1',
376
+ 'display_identity_v1',
377
+ ] as const
378
+ export type Capability = (typeof CAPABILITIES)[number]
379
+
380
+ export interface KeyModifiers {
381
+ control: boolean
382
+ alt: boolean
383
+ shift: boolean
384
+ platform: boolean
385
+ function: boolean
386
+ }
387
+
388
+ export type PointerButton = 'left' | 'right' | 'middle' | 'back' | 'forward'
389
+ /** Why the host asks the application to close a popup window overlay. */
390
+ export type DismissReason = 'window_blur'
391
+ /** What moved a split divider (`split_resize_v1`). */
392
+ export type ResizeSource = 'pointer' | 'keyboard'
393
+
394
+ interface NodeEventRoute {
395
+ sequence: number
396
+ window_id: number
397
+ node_id: number
398
+ tree_revision: number
399
+ /** Native committed ancestry, root first and target last. MAX_DEPTH counts edges. */
400
+ path: readonly number[]
401
+ }
402
+
403
+ type RoutedEvent = NodeEventRoute &
404
+ (
405
+ | { type: 'click' }
406
+ | { type: 'focus' }
407
+ | { type: 'blur' }
408
+ | { type: 'input'; value: string; input_revision: number }
409
+ | {
410
+ type: 'key_down'
411
+ key: string
412
+ key_char?: string
413
+ modifiers: KeyModifiers
414
+ }
415
+ | {
416
+ type: 'key_up'
417
+ key: string
418
+ key_char?: string
419
+ modifiers: KeyModifiers
420
+ }
421
+ | {
422
+ type: 'pointer_down'
423
+ button: PointerButton
424
+ x: number
425
+ y: number
426
+ modifiers: KeyModifiers
427
+ }
428
+ | {
429
+ type: 'pointer_up'
430
+ button: PointerButton
431
+ x: number
432
+ y: number
433
+ modifiers: KeyModifiers
434
+ }
435
+ | { type: 'dismiss'; reason: DismissReason }
436
+ | { type: 'split_resize'; ratio: number; source: ResizeSource }
437
+ )
438
+
439
+ export type NativeEvent =
440
+ | {
441
+ type: 'ready'
442
+ protocol: typeof PROTOCOL_VERSION
443
+ capabilities: Capability[]
444
+ limits: Limits
445
+ service_policy: ServicePolicy
446
+ }
447
+ | { type: 'ack'; id: number }
448
+ | {
449
+ type: 'opened'
450
+ id: number
451
+ window_id: number
452
+ root_id: number
453
+ tree_revision: number
454
+ }
455
+ | { type: 'applied'; id: number; window_id: number; tree_revision: number }
456
+ | {
457
+ type: 'display_snapshot'
458
+ id: number
459
+ window_id: number
460
+ snapshot: DisplaySnapshot
461
+ }
462
+ | {
463
+ type: 'clipboard_text'
464
+ id: number
465
+ window_id: number
466
+ result: ClipboardText
467
+ }
468
+ | { type: 'clipboard_written'; id: number; window_id: number }
469
+ | {
470
+ type: 'global_shortcut_registered'
471
+ id: number
472
+ window_id: number
473
+ shortcut_id: number
474
+ }
475
+ | {
476
+ type: 'global_shortcut_unregistered'
477
+ id: number
478
+ window_id: number
479
+ shortcut_id: number
480
+ }
481
+ | RoutedEvent
482
+ | { type: 'display_changed'; sequence: number; window_id: number; snapshot: DisplaySnapshot }
483
+ | { type: 'global_shortcut'; sequence: number; window_id: number; shortcut_id: number }
484
+ | {
485
+ type: 'notification_posted'
486
+ id: number
487
+ window_id: number
488
+ notification_id: number
489
+ activatable: boolean
490
+ }
491
+ | {
492
+ type: 'notification_withdrawn'
493
+ id: number
494
+ window_id: number
495
+ notification_id: number
496
+ }
497
+ | {
498
+ type: 'notification_activated'
499
+ sequence: number
500
+ window_id: number
501
+ notification_id: number
502
+ }
503
+ | {
504
+ type: 'notification_dismissed'
505
+ sequence: number
506
+ window_id: number
507
+ notification_id: number
508
+ reason: NotificationDismissal
509
+ }
510
+ | { type: 'file_grant_opened'; id: number; window_id: number; grant_id: number }
511
+ | { type: 'file_grant_closed'; id: number; window_id: number; grant_id: number }
512
+ | { type: 'file_result'; id: number; window_id: number; grant_id: number; result: FileResult }
513
+ | { type: 'process_spawned'; id: number; window_id: number; process_id: number }
514
+ | { type: 'process_updated'; id: number; window_id: number; process_id: number }
515
+ | { type: 'process_released'; id: number; window_id: number; process_id: number }
516
+ | {
517
+ type: 'process_output'
518
+ sequence: number
519
+ window_id: number
520
+ process_id: number
521
+ chunk: number
522
+ stream: ProcessStream
523
+ data: string
524
+ }
525
+ | {
526
+ type: 'process_exited'
527
+ sequence: number
528
+ window_id: number
529
+ process_id: number
530
+ exit: ProcessExit
531
+ }
532
+ | {
533
+ type: 'file_dialog_opened'
534
+ id: number
535
+ window_id: number
536
+ dialog_id: number
537
+ }
538
+ | {
539
+ type: 'file_dialog_closed'
540
+ id: number
541
+ window_id: number
542
+ dialog_id: number
543
+ }
544
+ | {
545
+ type: 'file_dialog_completed'
546
+ sequence: number
547
+ window_id: number
548
+ dialog_id: number
549
+ result: FileDialogResult
550
+ }
551
+ | {
552
+ type: 'message_dialog_opened'
553
+ id: number
554
+ window_id: number
555
+ dialog_id: number
556
+ }
557
+ | {
558
+ type: 'message_dialog_closed'
559
+ id: number
560
+ window_id: number
561
+ dialog_id: number
562
+ }
563
+ | {
564
+ type: 'message_dialog_completed'
565
+ sequence: number
566
+ window_id: number
567
+ dialog_id: number
568
+ result: MessageDialogResult
569
+ }
570
+ | { type: 'context_menu_opened'; id: number; window_id: number; menu_id: number }
571
+ | { type: 'context_menu_closed'; id: number; window_id: number; menu_id: number }
572
+ /** `item_id` is absent when the menu was dismissed. */
573
+ | {
574
+ type: 'context_menu_completed'
575
+ sequence: number
576
+ window_id: number
577
+ menu_id: number
578
+ item_id?: string
579
+ }
580
+ | { type: 'drop_target_registered'; id: number; window_id: number; target_id: number }
581
+ | { type: 'drop_target_unregistered'; id: number; window_id: number; target_id: number }
582
+ | {
583
+ type: 'dropped_item_resolved'
584
+ id: number
585
+ window_id: number
586
+ target_id: number
587
+ drop_id: number
588
+ item: number
589
+ root: string
590
+ path: string
591
+ }
592
+ | {
593
+ type: 'drop_target_drag'
594
+ sequence: number
595
+ window_id: number
596
+ target_id: number
597
+ state: DragState
598
+ }
599
+ | {
600
+ type: 'drop_received'
601
+ sequence: number
602
+ window_id: number
603
+ target_id: number
604
+ drop_id: number
605
+ items: DroppedItem[]
606
+ }
607
+ | {
608
+ type: 'drop_rejected'
609
+ sequence: number
610
+ window_id: number
611
+ target_id: number
612
+ reason: DropRejection
613
+ }
614
+ | { type: 'closed'; sequence: number; window_id: number }
615
+ | { type: 'app_menu_set'; id: number; menu_id: number }
616
+ /** Application-scoped: a menu bar belongs to the application, not a window. */
617
+ | { type: 'app_menu_item'; sequence: number; menu_id: number; item_id: string }
618
+ | { type: 'error'; id?: number; code: ErrorCode; message: string }
619
+ /** Sequenced events owned by one window. */
620
+ export type WindowEvent = Extract<NativeEvent, { sequence: number; window_id: number }>
621
+ /** Sequenced events owned by the application rather than one window. */
622
+ export type ApplicationEvent = Extract<NativeEvent, { type: 'app_menu_item' }>
623
+ /** Unsolicited events, as opposed to responses that carry a request id. */
624
+ export type HostEvent = WindowEvent | ApplicationEvent
625
+
626
+ function version(value: unknown): typeof PROTOCOL_VERSION {
627
+ if (value !== PROTOCOL_VERSION) throw new ProtocolError('Native protocol version mismatch')
628
+ return PROTOCOL_VERSION
629
+ }
630
+
631
+ /** Property names of a window-scoped command, reply or unsolicited event. */
632
+ const windowCommand = (...names: string[]) => fields('id', 'type', 'window_id', ...names)
633
+ const windowReply = (...names: string[]) => fields('type', 'id', 'window_id', ...names)
634
+ const windowNotice = (...names: string[]) => fields('type', 'sequence', 'window_id', ...names)
635
+
636
+ const OPERATION_CREATE_KEYS = fields('type', 'node_id', 'element')
637
+ const OPERATION_SET_KEYS = fields('type', 'node_id', 'element', 'expected_input_revision')
638
+ const OPERATION_INSERT_KEYS = fields('type', 'parent_id', 'child_id', 'index')
639
+ const OPERATION_DELETE_KEYS = fields('type', 'node_id')
640
+ const OPERATION_SCROLL_TO_KEYS = fields('type', 'node_id', 'x', 'y')
641
+
642
+ function operation(value: unknown): Operation {
643
+ const data = object(value)
644
+ switch (data['type']) {
645
+ case 'create':
646
+ keys(data, OPERATION_CREATE_KEYS)
647
+ return {
648
+ type: 'create',
649
+ node_id: id(data['node_id']),
650
+ element: parseElement(data['element']),
651
+ }
652
+ case 'set': {
653
+ keys(data, OPERATION_SET_KEYS)
654
+ const node_id = id(data['node_id'])
655
+ const element = parseElement(data['element'])
656
+ if (element.kind === 'input')
657
+ return {
658
+ type: 'set',
659
+ node_id,
660
+ element,
661
+ expected_input_revision: integer(data['expected_input_revision'], 'input revision'),
662
+ }
663
+ if ('expected_input_revision' in data)
664
+ throw new ProtocolError('Only input elements accept an input revision')
665
+ return { type: 'set', node_id, element }
666
+ }
667
+ case 'insert':
668
+ keys(data, OPERATION_INSERT_KEYS)
669
+ return {
670
+ type: 'insert',
671
+ parent_id: id(data['parent_id']),
672
+ child_id: id(data['child_id']),
673
+ index: integer(data['index'], 'index', 0, MAX_NODES_PER_WINDOW),
674
+ }
675
+ case 'delete':
676
+ case 'focus':
677
+ case 'scroll_into_view':
678
+ keys(data, OPERATION_DELETE_KEYS)
679
+ return { type: data['type'], node_id: id(data['node_id']) }
680
+ case 'scroll_to': {
681
+ keys(data, OPERATION_SCROLL_TO_KEYS)
682
+ if (!('x' in data) && !('y' in data)) throw new ProtocolError('scroll_to requires x or y')
683
+ const offset = (axis: 'x' | 'y') =>
684
+ axis in data ? { [axis]: number(data[axis], 'scroll ' + axis, 0, MAX_SCROLL_OFFSET) } : {}
685
+ return { type: 'scroll_to', node_id: id(data['node_id']), ...offset('x'), ...offset('y') }
686
+ }
687
+ default:
688
+ throw new ProtocolError('Unknown native operation')
689
+ }
690
+ }
691
+
692
+ const COMMAND_HELLO_KEYS = fields('id', 'type', 'protocol')
693
+ const COMMAND_OPEN_KEYS = windowCommand('root_id', 'title', 'width', 'height')
694
+ const COMMAND_BATCH_KEYS = windowCommand('base_revision', 'operations')
695
+ const COMMAND_CLOSE_KEYS = windowCommand()
696
+ const COMMAND_CLIPBOARD_WRITE_KEYS = windowCommand('text')
697
+ const COMMAND_REGISTER_GLOBAL_SHORTCUT_KEYS = windowCommand('shortcut_id', 'key', 'modifiers')
698
+ const COMMAND_UNREGISTER_GLOBAL_SHORTCUT_KEYS = windowCommand('shortcut_id')
699
+ const COMMAND_POST_NOTIFICATION_KEYS = windowCommand('notification_id', 'title', 'body')
700
+ const COMMAND_WITHDRAW_NOTIFICATION_KEYS = windowCommand('notification_id')
701
+ const COMMAND_OPEN_FILE_GRANT_KEYS = windowCommand('grant_id', 'root', 'access')
702
+ const COMMAND_CLOSE_FILE_GRANT_KEYS = windowCommand('grant_id')
703
+ const COMMAND_FILE_REQUEST_KEYS = windowCommand('grant_id', 'path', 'operation')
704
+ const COMMAND_SPAWN_PROCESS_KEYS = windowCommand('process_id', 'launch')
705
+ const COMMAND_WRITE_PROCESS_KEYS = windowCommand('process_id', 'data')
706
+ const COMMAND_CLOSE_PROCESS_INPUT_KEYS = windowCommand('process_id')
707
+ const COMMAND_RESIZE_PROCESS_KEYS = windowCommand('process_id', 'size')
708
+ const COMMAND_SIGNAL_PROCESS_KEYS = windowCommand('process_id', 'signal')
709
+ const COMMAND_ACKNOWLEDGE_PROCESS_OUTPUT_KEYS = windowCommand('process_id', 'chunk')
710
+ const COMMAND_SHOW_FILE_DIALOG_KEYS = windowCommand('dialog_id', 'options')
711
+ const COMMAND_CLOSE_FILE_DIALOG_KEYS = windowCommand('dialog_id')
712
+ const COMMAND_SHOW_MESSAGE_DIALOG_KEYS = windowCommand('dialog_id', 'options')
713
+ const COMMAND_CLOSE_MESSAGE_DIALOG_KEYS = windowCommand('dialog_id')
714
+ const COMMAND_SHOW_CONTEXT_MENU_KEYS = windowCommand('menu_id', 'x', 'y', 'items')
715
+ const COMMAND_CLOSE_CONTEXT_MENU_KEYS = windowCommand('menu_id')
716
+ const COMMAND_REGISTER_DROP_TARGET_KEYS = windowCommand('target_id', 'node_id')
717
+ const COMMAND_UNREGISTER_DROP_TARGET_KEYS = windowCommand('target_id')
718
+ const COMMAND_RESOLVE_DROPPED_ITEM_KEYS = windowCommand('target_id', 'drop_id', 'item', 'root')
719
+ const COMMAND_SET_APP_MENU_KEYS = fields('id', 'type', 'menu_id', 'menus')
720
+ const COMMAND_SHUTDOWN_KEYS = fields('id', 'type')
721
+
722
+ /** Validate before crossing the native boundary, including JavaScript callers. */
723
+ export function parseCommand(value: unknown): Command {
724
+ const message = object(value)
725
+ const request = id(message['id'])
726
+ switch (message['type']) {
727
+ case 'hello':
728
+ keys(message, COMMAND_HELLO_KEYS)
729
+ return {
730
+ id: request,
731
+ type: 'hello',
732
+ protocol: version(message['protocol']),
733
+ }
734
+ case 'open': {
735
+ keys(message, COMMAND_OPEN_KEYS)
736
+ const title = string(message['title'], 'title', MAX_TITLE_BYTES)
737
+ if (!title || /\p{Cc}/u.test(title))
738
+ throw new ProtocolError('Window title must be nonempty and contain no control characters')
739
+ return {
740
+ id: request,
741
+ type: 'open',
742
+ window_id: id(message['window_id']),
743
+ root_id: id(message['root_id']),
744
+ title,
745
+ width: number(message['width'], 'width', 64, 16_384),
746
+ height: number(message['height'], 'height', 64, 16_384),
747
+ }
748
+ }
749
+ case 'batch': {
750
+ keys(message, COMMAND_BATCH_KEYS)
751
+ const operations = message['operations']
752
+ if (!Array.isArray(operations) || operations.length < 1 || operations.length > MAX_OPERATIONS)
753
+ throw new ProtocolError(
754
+ 'Batch must contain between 1 and ' + MAX_OPERATIONS + ' operations'
755
+ )
756
+ return {
757
+ id: request,
758
+ type: 'batch',
759
+ window_id: id(message['window_id']),
760
+ base_revision: integer(message['base_revision'], 'tree revision'),
761
+ operations: operations.map(operation),
762
+ }
763
+ }
764
+ case 'close':
765
+ case 'display_snapshot':
766
+ case 'clipboard_read':
767
+ keys(message, COMMAND_CLOSE_KEYS)
768
+ return {
769
+ id: request,
770
+ type: message['type'],
771
+ window_id: id(message['window_id']),
772
+ }
773
+ case 'clipboard_write':
774
+ keys(message, COMMAND_CLIPBOARD_WRITE_KEYS)
775
+ return {
776
+ id: request,
777
+ type: 'clipboard_write',
778
+ window_id: id(message['window_id']),
779
+ text: string(message['text'], 'clipboard text', MAX_CLIPBOARD_BYTES),
780
+ }
781
+ case 'register_global_shortcut':
782
+ keys(message, COMMAND_REGISTER_GLOBAL_SHORTCUT_KEYS)
783
+ return {
784
+ id: request,
785
+ type: 'register_global_shortcut',
786
+ window_id: id(message['window_id']),
787
+ shortcut_id: id(message['shortcut_id']),
788
+ key: parseShortcutKey(message['key']),
789
+ modifiers: parseShortcutModifiers(message['modifiers']),
790
+ }
791
+ case 'unregister_global_shortcut':
792
+ keys(message, COMMAND_UNREGISTER_GLOBAL_SHORTCUT_KEYS)
793
+ return {
794
+ id: request,
795
+ type: 'unregister_global_shortcut',
796
+ window_id: id(message['window_id']),
797
+ shortcut_id: id(message['shortcut_id']),
798
+ }
799
+ case 'post_notification': {
800
+ keys(message, COMMAND_POST_NOTIFICATION_KEYS)
801
+ const { title, body } = parseNotificationContent(message['title'], message['body'])
802
+ return {
803
+ id: request,
804
+ type: 'post_notification',
805
+ window_id: id(message['window_id']),
806
+ notification_id: id(message['notification_id']),
807
+ title,
808
+ body,
809
+ }
810
+ }
811
+ case 'withdraw_notification':
812
+ keys(message, COMMAND_WITHDRAW_NOTIFICATION_KEYS)
813
+ return {
814
+ id: request,
815
+ type: 'withdraw_notification',
816
+ window_id: id(message['window_id']),
817
+ notification_id: id(message['notification_id']),
818
+ }
819
+ case 'open_file_grant':
820
+ keys(message, COMMAND_OPEN_FILE_GRANT_KEYS)
821
+ return {
822
+ id: request,
823
+ type: 'open_file_grant',
824
+ window_id: id(message['window_id']),
825
+ grant_id: id(message['grant_id']),
826
+ root: parseRootId(message['root']),
827
+ access: parseFileAccess(message['access']),
828
+ }
829
+ case 'close_file_grant':
830
+ keys(message, COMMAND_CLOSE_FILE_GRANT_KEYS)
831
+ return {
832
+ id: request,
833
+ type: 'close_file_grant',
834
+ window_id: id(message['window_id']),
835
+ grant_id: id(message['grant_id']),
836
+ }
837
+ case 'file_request': {
838
+ keys(message, COMMAND_FILE_REQUEST_KEYS)
839
+ const path = parseFilePath(message['path'])
840
+ return {
841
+ id: request,
842
+ type: 'file_request',
843
+ window_id: id(message['window_id']),
844
+ grant_id: id(message['grant_id']),
845
+ path,
846
+ operation: parseFileOperation(message['operation'], path),
847
+ }
848
+ }
849
+ case 'spawn_process':
850
+ keys(message, COMMAND_SPAWN_PROCESS_KEYS)
851
+ return {
852
+ id: request,
853
+ type: 'spawn_process',
854
+ window_id: id(message['window_id']),
855
+ process_id: id(message['process_id']),
856
+ launch: parseProcessLaunch(message['launch']),
857
+ }
858
+ case 'write_process':
859
+ keys(message, COMMAND_WRITE_PROCESS_KEYS)
860
+ return {
861
+ id: request,
862
+ type: 'write_process',
863
+ window_id: id(message['window_id']),
864
+ process_id: id(message['process_id']),
865
+ data: parseProcessOutput(message['data']),
866
+ }
867
+ case 'close_process_input':
868
+ case 'release_process':
869
+ keys(message, COMMAND_CLOSE_PROCESS_INPUT_KEYS)
870
+ return {
871
+ id: request,
872
+ type: message['type'],
873
+ window_id: id(message['window_id']),
874
+ process_id: id(message['process_id']),
875
+ }
876
+ case 'resize_process':
877
+ keys(message, COMMAND_RESIZE_PROCESS_KEYS)
878
+ return {
879
+ id: request,
880
+ type: 'resize_process',
881
+ window_id: id(message['window_id']),
882
+ process_id: id(message['process_id']),
883
+ size: parseTerminalSize(message['size']),
884
+ }
885
+ case 'signal_process':
886
+ keys(message, COMMAND_SIGNAL_PROCESS_KEYS)
887
+ return {
888
+ id: request,
889
+ type: 'signal_process',
890
+ window_id: id(message['window_id']),
891
+ process_id: id(message['process_id']),
892
+ signal: parseProcessSignal(message['signal']),
893
+ }
894
+ case 'acknowledge_process_output':
895
+ keys(message, COMMAND_ACKNOWLEDGE_PROCESS_OUTPUT_KEYS)
896
+ return {
897
+ id: request,
898
+ type: 'acknowledge_process_output',
899
+ window_id: id(message['window_id']),
900
+ process_id: id(message['process_id']),
901
+ chunk: id(message['chunk']),
902
+ }
903
+ case 'show_file_dialog':
904
+ keys(message, COMMAND_SHOW_FILE_DIALOG_KEYS)
905
+ return {
906
+ id: request,
907
+ type: 'show_file_dialog',
908
+ window_id: id(message['window_id']),
909
+ dialog_id: id(message['dialog_id']),
910
+ options: parseFileDialogOptions(message['options']),
911
+ }
912
+ case 'close_file_dialog':
913
+ keys(message, COMMAND_CLOSE_FILE_DIALOG_KEYS)
914
+ return {
915
+ id: request,
916
+ type: 'close_file_dialog',
917
+ window_id: id(message['window_id']),
918
+ dialog_id: id(message['dialog_id']),
919
+ }
920
+ case 'show_message_dialog':
921
+ keys(message, COMMAND_SHOW_MESSAGE_DIALOG_KEYS)
922
+ return {
923
+ id: request,
924
+ type: 'show_message_dialog',
925
+ window_id: id(message['window_id']),
926
+ dialog_id: id(message['dialog_id']),
927
+ options: parseMessageDialogOptions(message['options']),
928
+ }
929
+ case 'close_message_dialog':
930
+ keys(message, COMMAND_CLOSE_MESSAGE_DIALOG_KEYS)
931
+ return {
932
+ id: request,
933
+ type: 'close_message_dialog',
934
+ window_id: id(message['window_id']),
935
+ dialog_id: id(message['dialog_id']),
936
+ }
937
+ case 'show_context_menu':
938
+ keys(message, COMMAND_SHOW_CONTEXT_MENU_KEYS)
939
+ return {
940
+ id: request,
941
+ type: 'show_context_menu',
942
+ window_id: id(message['window_id']),
943
+ menu_id: id(message['menu_id']),
944
+ x: number(message['x'], 'context menu x', 0, 1_048_576),
945
+ y: number(message['y'], 'context menu y', 0, 1_048_576),
946
+ items: parseContextMenuItems(message['items']),
947
+ }
948
+ case 'close_context_menu':
949
+ keys(message, COMMAND_CLOSE_CONTEXT_MENU_KEYS)
950
+ return {
951
+ id: request,
952
+ type: 'close_context_menu',
953
+ window_id: id(message['window_id']),
954
+ menu_id: id(message['menu_id']),
955
+ }
956
+ case 'register_drop_target':
957
+ keys(message, COMMAND_REGISTER_DROP_TARGET_KEYS)
958
+ return {
959
+ id: request,
960
+ type: 'register_drop_target',
961
+ window_id: id(message['window_id']),
962
+ target_id: id(message['target_id']),
963
+ node_id: id(message['node_id']),
964
+ }
965
+ case 'unregister_drop_target':
966
+ keys(message, COMMAND_UNREGISTER_DROP_TARGET_KEYS)
967
+ return {
968
+ id: request,
969
+ type: 'unregister_drop_target',
970
+ window_id: id(message['window_id']),
971
+ target_id: id(message['target_id']),
972
+ }
973
+ case 'resolve_dropped_item':
974
+ keys(message, COMMAND_RESOLVE_DROPPED_ITEM_KEYS)
975
+ return {
976
+ id: request,
977
+ type: 'resolve_dropped_item',
978
+ window_id: id(message['window_id']),
979
+ target_id: id(message['target_id']),
980
+ drop_id: id(message['drop_id']),
981
+ item: parseDropItemIndex(message['item']),
982
+ root: parseRootId(message['root']),
983
+ }
984
+ case 'set_app_menu':
985
+ keys(message, COMMAND_SET_APP_MENU_KEYS)
986
+ return {
987
+ id: request,
988
+ type: 'set_app_menu',
989
+ menu_id: id(message['menu_id']),
990
+ menus: parseAppMenus(message['menus']),
991
+ }
992
+ case 'shutdown':
993
+ keys(message, COMMAND_SHUTDOWN_KEYS)
994
+ return { id: request, type: 'shutdown' }
995
+ default:
996
+ throw new ProtocolError('Unknown native command')
997
+ }
998
+ }
999
+
1000
+ const LIMITS_KEYS = fields(...Object.keys(DEFAULT_LIMITS))
1001
+
1002
+ function limits(value: unknown): Limits {
1003
+ const data = object(value)
1004
+ keys(data, LIMITS_KEYS)
1005
+ const parse = (key: keyof Limits) => integer(data[key], key, 1, DEFAULT_LIMITS[key])
1006
+ return {
1007
+ max_frame_bytes: parse('max_frame_bytes'),
1008
+ max_operations: parse('max_operations'),
1009
+ max_nodes_per_window: parse('max_nodes_per_window'),
1010
+ max_windows: parse('max_windows'),
1011
+ max_depth: parse('max_depth'),
1012
+ max_text_bytes: parse('max_text_bytes'),
1013
+ max_text_bytes_per_window: parse('max_text_bytes_per_window'),
1014
+ max_clipboard_bytes: parse('max_clipboard_bytes'),
1015
+ max_service_requests: parse('max_service_requests'),
1016
+ max_global_shortcuts: parse('max_global_shortcuts'),
1017
+ max_notifications: parse('max_notifications'),
1018
+ max_file_grants: parse('max_file_grants'),
1019
+ max_file_chunk_bytes: parse('max_file_chunk_bytes'),
1020
+ max_processes: parse('max_processes'),
1021
+ max_drop_targets: parse('max_drop_targets'),
1022
+ max_drop_items: parse('max_drop_items'),
1023
+ }
1024
+ }
1025
+
1026
+ const ROUTE_KEYS = ['type', 'sequence', 'window_id', 'node_id', 'tree_revision', 'path'] as const
1027
+
1028
+ /** Short paths, the common case, compare pairwise instead of building a set. */
1029
+ function unique(path: readonly number[]): boolean {
1030
+ if (path.length > 16) return new Set(path).size === path.length
1031
+ for (let index = 1; index < path.length; index++)
1032
+ for (let other = 0; other < index; other++) if (path[index] === path[other]) return false
1033
+ return true
1034
+ }
1035
+
1036
+ function route(message: Record<string, unknown>): NodeEventRoute {
1037
+ const node_id = id(message['node_id'])
1038
+ const value = message['path']
1039
+ if (!Array.isArray(value) || !value.length || value.length > MAX_DEPTH + 1)
1040
+ throw new ProtocolError('Native event path must contain between 1 and 65 node identities')
1041
+ const path = value.map(id)
1042
+ if (!unique(path) || path.at(-1) !== node_id)
1043
+ throw new ProtocolError('Native event path must be unique and end at its target')
1044
+ return {
1045
+ sequence: id(message['sequence']),
1046
+ window_id: id(message['window_id']),
1047
+ node_id,
1048
+ tree_revision: integer(message['tree_revision'], 'tree revision'),
1049
+ path,
1050
+ }
1051
+ }
1052
+
1053
+ const MODIFIERS_KEYS = fields('control', 'alt', 'shift', 'platform', 'function')
1054
+
1055
+ function modifiers(value: unknown): KeyModifiers {
1056
+ const data = object(value)
1057
+ keys(data, MODIFIERS_KEYS)
1058
+ const modifier = (name: keyof KeyModifiers) => {
1059
+ if (typeof data[name] !== 'boolean') throw new ProtocolError('Invalid native key modifier')
1060
+ return data[name]
1061
+ }
1062
+ return {
1063
+ control: modifier('control'),
1064
+ alt: modifier('alt'),
1065
+ shift: modifier('shift'),
1066
+ platform: modifier('platform'),
1067
+ function: modifier('function'),
1068
+ }
1069
+ }
1070
+
1071
+ const EVENT_READY_KEYS = fields('type', 'protocol', 'capabilities', 'limits', 'service_policy')
1072
+ const EVENT_ACK_KEYS = fields('type', 'id')
1073
+ const EVENT_OPENED_KEYS = windowReply('root_id', 'tree_revision')
1074
+ const EVENT_APPLIED_KEYS = windowReply('tree_revision')
1075
+ const EVENT_DISPLAY_SNAPSHOT_KEYS = windowReply('snapshot')
1076
+ const EVENT_CLIPBOARD_TEXT_KEYS = windowReply('result')
1077
+ const EVENT_CLIPBOARD_WRITTEN_KEYS = windowReply()
1078
+ const EVENT_GLOBAL_SHORTCUT_REGISTERED_KEYS = windowReply('shortcut_id')
1079
+ const EVENT_DISPLAY_CHANGED_KEYS = windowNotice('snapshot')
1080
+ const EVENT_GLOBAL_SHORTCUT_KEYS = windowNotice('shortcut_id')
1081
+ const EVENT_NOTIFICATION_POSTED_KEYS = windowReply('notification_id', 'activatable')
1082
+ const EVENT_NOTIFICATION_WITHDRAWN_KEYS = windowReply('notification_id')
1083
+ const EVENT_NOTIFICATION_ACTIVATED_KEYS = windowNotice('notification_id')
1084
+ const EVENT_NOTIFICATION_DISMISSED_KEYS = windowNotice('notification_id', 'reason')
1085
+ const EVENT_FILE_GRANT_OPENED_KEYS = windowReply('grant_id')
1086
+ const EVENT_FILE_RESULT_KEYS = windowReply('grant_id', 'result')
1087
+ const EVENT_PROCESS_SPAWNED_KEYS = windowReply('process_id')
1088
+ const EVENT_PROCESS_OUTPUT_KEYS = windowNotice('process_id', 'chunk', 'stream', 'data')
1089
+ const EVENT_PROCESS_EXITED_KEYS = windowNotice('process_id', 'exit')
1090
+ const EVENT_FILE_DIALOG_OPENED_KEYS = windowReply('dialog_id')
1091
+ const EVENT_FILE_DIALOG_COMPLETED_KEYS = windowNotice('dialog_id', 'result')
1092
+ const EVENT_MESSAGE_DIALOG_OPENED_KEYS = windowReply('dialog_id')
1093
+ const EVENT_MESSAGE_DIALOG_COMPLETED_KEYS = windowNotice('dialog_id', 'result')
1094
+ const EVENT_CONTEXT_MENU_OPENED_KEYS = windowReply('menu_id')
1095
+ const EVENT_CONTEXT_MENU_COMPLETED_KEYS = windowNotice('menu_id', 'item_id')
1096
+ const EVENT_DROP_TARGET_REGISTERED_KEYS = windowReply('target_id')
1097
+ const EVENT_DROPPED_ITEM_RESOLVED_KEYS = windowReply('target_id', 'drop_id', 'item', 'root', 'path')
1098
+ const EVENT_DROP_TARGET_DRAG_KEYS = windowNotice('target_id', 'state')
1099
+ const EVENT_DROP_RECEIVED_KEYS = windowNotice('target_id', 'drop_id', 'items')
1100
+ const EVENT_DROP_REJECTED_KEYS = windowNotice('target_id', 'reason')
1101
+ const EVENT_ROUTE_KEYS = fields(...ROUTE_KEYS)
1102
+ const EVENT_KEY_KEYS = fields(...ROUTE_KEYS, 'key', 'key_char', 'modifiers')
1103
+ const EVENT_POINTER_KEYS = fields(...ROUTE_KEYS, 'button', 'x', 'y', 'modifiers')
1104
+ const EVENT_DISMISS_KEYS = fields(...ROUTE_KEYS, 'reason')
1105
+ const EVENT_SPLIT_RESIZE_KEYS = fields(...ROUTE_KEYS, 'ratio', 'source')
1106
+ const EVENT_CLOSED_KEYS = windowNotice()
1107
+ const EVENT_INPUT_KEYS = fields(...ROUTE_KEYS, 'value', 'input_revision')
1108
+ const EVENT_APP_MENU_SET_KEYS = fields('type', 'id', 'menu_id')
1109
+ const EVENT_APP_MENU_ITEM_KEYS = fields('type', 'sequence', 'menu_id', 'item_id')
1110
+ const EVENT_ERROR_KEYS = fields('type', 'id', 'code', 'message')
1111
+
1112
+ /** Native stdout is untrusted until its message shape and limits are checked. */
1113
+ export function parseEvent(value: unknown): NativeEvent {
1114
+ const message = object(value)
1115
+ switch (message['type']) {
1116
+ case 'ready': {
1117
+ keys(message, EVENT_READY_KEYS)
1118
+ const capabilities = message['capabilities']
1119
+ if (!Array.isArray(capabilities) || capabilities.length > CAPABILITIES.length)
1120
+ throw new ProtocolError('Invalid native capabilities')
1121
+ const parsed: Capability[] = []
1122
+ for (const value of capabilities) {
1123
+ const capability = CAPABILITIES.find((candidate) => candidate === value)
1124
+ if (!capability || parsed.includes(capability))
1125
+ throw new ProtocolError('Unknown or duplicate native capability')
1126
+ parsed.push(capability)
1127
+ }
1128
+ return {
1129
+ type: 'ready',
1130
+ protocol: version(message['protocol']),
1131
+ capabilities: parsed,
1132
+ limits: limits(message['limits']),
1133
+ service_policy: parseServicePolicy(message['service_policy']),
1134
+ }
1135
+ }
1136
+ case 'ack':
1137
+ keys(message, EVENT_ACK_KEYS)
1138
+ return { type: 'ack', id: id(message['id']) }
1139
+ case 'opened':
1140
+ keys(message, EVENT_OPENED_KEYS)
1141
+ return {
1142
+ type: 'opened',
1143
+ id: id(message['id']),
1144
+ window_id: id(message['window_id']),
1145
+ root_id: id(message['root_id']),
1146
+ tree_revision: integer(message['tree_revision'], 'tree revision'),
1147
+ }
1148
+ case 'applied':
1149
+ keys(message, EVENT_APPLIED_KEYS)
1150
+ return {
1151
+ type: 'applied',
1152
+ id: id(message['id']),
1153
+ window_id: id(message['window_id']),
1154
+ tree_revision: integer(message['tree_revision'], 'tree revision'),
1155
+ }
1156
+ case 'display_snapshot':
1157
+ keys(message, EVENT_DISPLAY_SNAPSHOT_KEYS)
1158
+ return {
1159
+ type: 'display_snapshot',
1160
+ id: id(message['id']),
1161
+ window_id: id(message['window_id']),
1162
+ snapshot: parseDisplaySnapshot(message['snapshot']),
1163
+ }
1164
+ case 'clipboard_text':
1165
+ keys(message, EVENT_CLIPBOARD_TEXT_KEYS)
1166
+ return {
1167
+ type: 'clipboard_text',
1168
+ id: id(message['id']),
1169
+ window_id: id(message['window_id']),
1170
+ result: parseClipboardText(message['result']),
1171
+ }
1172
+ case 'clipboard_written':
1173
+ keys(message, EVENT_CLIPBOARD_WRITTEN_KEYS)
1174
+ return {
1175
+ type: 'clipboard_written',
1176
+ id: id(message['id']),
1177
+ window_id: id(message['window_id']),
1178
+ }
1179
+ case 'global_shortcut_registered':
1180
+ case 'global_shortcut_unregistered':
1181
+ keys(message, EVENT_GLOBAL_SHORTCUT_REGISTERED_KEYS)
1182
+ return {
1183
+ type: message['type'],
1184
+ id: id(message['id']),
1185
+ window_id: id(message['window_id']),
1186
+ shortcut_id: id(message['shortcut_id']),
1187
+ }
1188
+ case 'display_changed':
1189
+ keys(message, EVENT_DISPLAY_CHANGED_KEYS)
1190
+ return {
1191
+ type: 'display_changed',
1192
+ sequence: id(message['sequence']),
1193
+ window_id: id(message['window_id']),
1194
+ snapshot: parseDisplaySnapshot(message['snapshot']),
1195
+ }
1196
+ case 'global_shortcut':
1197
+ keys(message, EVENT_GLOBAL_SHORTCUT_KEYS)
1198
+ return {
1199
+ type: 'global_shortcut',
1200
+ sequence: id(message['sequence']),
1201
+ window_id: id(message['window_id']),
1202
+ shortcut_id: id(message['shortcut_id']),
1203
+ }
1204
+ case 'notification_posted':
1205
+ keys(message, EVENT_NOTIFICATION_POSTED_KEYS)
1206
+ if (typeof message['activatable'] !== 'boolean')
1207
+ throw new ProtocolError('Notification activatable must be a boolean')
1208
+ return {
1209
+ type: 'notification_posted',
1210
+ id: id(message['id']),
1211
+ window_id: id(message['window_id']),
1212
+ notification_id: id(message['notification_id']),
1213
+ activatable: message['activatable'],
1214
+ }
1215
+ case 'notification_withdrawn':
1216
+ keys(message, EVENT_NOTIFICATION_WITHDRAWN_KEYS)
1217
+ return {
1218
+ type: 'notification_withdrawn',
1219
+ id: id(message['id']),
1220
+ window_id: id(message['window_id']),
1221
+ notification_id: id(message['notification_id']),
1222
+ }
1223
+ case 'notification_activated':
1224
+ keys(message, EVENT_NOTIFICATION_ACTIVATED_KEYS)
1225
+ return {
1226
+ type: 'notification_activated',
1227
+ sequence: id(message['sequence']),
1228
+ window_id: id(message['window_id']),
1229
+ notification_id: id(message['notification_id']),
1230
+ }
1231
+ case 'notification_dismissed':
1232
+ keys(message, EVENT_NOTIFICATION_DISMISSED_KEYS)
1233
+ return {
1234
+ type: 'notification_dismissed',
1235
+ sequence: id(message['sequence']),
1236
+ window_id: id(message['window_id']),
1237
+ notification_id: id(message['notification_id']),
1238
+ reason: parseNotificationDismissal(message['reason']),
1239
+ }
1240
+ case 'file_grant_opened':
1241
+ case 'file_grant_closed':
1242
+ keys(message, EVENT_FILE_GRANT_OPENED_KEYS)
1243
+ return {
1244
+ type: message['type'],
1245
+ id: id(message['id']),
1246
+ window_id: id(message['window_id']),
1247
+ grant_id: id(message['grant_id']),
1248
+ }
1249
+ case 'file_result':
1250
+ keys(message, EVENT_FILE_RESULT_KEYS)
1251
+ return {
1252
+ type: 'file_result',
1253
+ id: id(message['id']),
1254
+ window_id: id(message['window_id']),
1255
+ grant_id: id(message['grant_id']),
1256
+ result: parseFileResult(message['result']),
1257
+ }
1258
+ case 'process_spawned':
1259
+ case 'process_updated':
1260
+ case 'process_released':
1261
+ keys(message, EVENT_PROCESS_SPAWNED_KEYS)
1262
+ return {
1263
+ type: message['type'],
1264
+ id: id(message['id']),
1265
+ window_id: id(message['window_id']),
1266
+ process_id: id(message['process_id']),
1267
+ }
1268
+ case 'process_output':
1269
+ keys(message, EVENT_PROCESS_OUTPUT_KEYS)
1270
+ return {
1271
+ type: 'process_output',
1272
+ sequence: id(message['sequence']),
1273
+ window_id: id(message['window_id']),
1274
+ process_id: id(message['process_id']),
1275
+ chunk: id(message['chunk']),
1276
+ stream: parseProcessStream(message['stream']),
1277
+ data: parseProcessOutput(message['data']),
1278
+ }
1279
+ case 'process_exited':
1280
+ keys(message, EVENT_PROCESS_EXITED_KEYS)
1281
+ return {
1282
+ type: 'process_exited',
1283
+ sequence: id(message['sequence']),
1284
+ window_id: id(message['window_id']),
1285
+ process_id: id(message['process_id']),
1286
+ exit: parseProcessExit(message['exit']),
1287
+ }
1288
+ case 'file_dialog_opened':
1289
+ case 'file_dialog_closed':
1290
+ keys(message, EVENT_FILE_DIALOG_OPENED_KEYS)
1291
+ return {
1292
+ type: message['type'],
1293
+ id: id(message['id']),
1294
+ window_id: id(message['window_id']),
1295
+ dialog_id: id(message['dialog_id']),
1296
+ }
1297
+ case 'file_dialog_completed':
1298
+ keys(message, EVENT_FILE_DIALOG_COMPLETED_KEYS)
1299
+ return {
1300
+ type: 'file_dialog_completed',
1301
+ sequence: id(message['sequence']),
1302
+ window_id: id(message['window_id']),
1303
+ dialog_id: id(message['dialog_id']),
1304
+ result: parseFileDialogResult(message['result']),
1305
+ }
1306
+ case 'message_dialog_opened':
1307
+ case 'message_dialog_closed':
1308
+ keys(message, EVENT_MESSAGE_DIALOG_OPENED_KEYS)
1309
+ return {
1310
+ type: message['type'],
1311
+ id: id(message['id']),
1312
+ window_id: id(message['window_id']),
1313
+ dialog_id: id(message['dialog_id']),
1314
+ }
1315
+ case 'message_dialog_completed':
1316
+ keys(message, EVENT_MESSAGE_DIALOG_COMPLETED_KEYS)
1317
+ return {
1318
+ type: 'message_dialog_completed',
1319
+ sequence: id(message['sequence']),
1320
+ window_id: id(message['window_id']),
1321
+ dialog_id: id(message['dialog_id']),
1322
+ result: parseMessageDialogResult(message['result']),
1323
+ }
1324
+ case 'context_menu_opened':
1325
+ case 'context_menu_closed':
1326
+ keys(message, EVENT_CONTEXT_MENU_OPENED_KEYS)
1327
+ return {
1328
+ type: message['type'],
1329
+ id: id(message['id']),
1330
+ window_id: id(message['window_id']),
1331
+ menu_id: id(message['menu_id']),
1332
+ }
1333
+ case 'context_menu_completed': {
1334
+ keys(message, EVENT_CONTEXT_MENU_COMPLETED_KEYS)
1335
+ const event: Extract<NativeEvent, { type: 'context_menu_completed' }> = {
1336
+ type: 'context_menu_completed',
1337
+ sequence: id(message['sequence']),
1338
+ window_id: id(message['window_id']),
1339
+ menu_id: id(message['menu_id']),
1340
+ }
1341
+ if ('item_id' in message) event.item_id = parseAppMenuItemId(message['item_id'])
1342
+ return event
1343
+ }
1344
+ case 'drop_target_registered':
1345
+ case 'drop_target_unregistered':
1346
+ keys(message, EVENT_DROP_TARGET_REGISTERED_KEYS)
1347
+ return {
1348
+ type: message['type'],
1349
+ id: id(message['id']),
1350
+ window_id: id(message['window_id']),
1351
+ target_id: id(message['target_id']),
1352
+ }
1353
+ case 'dropped_item_resolved':
1354
+ keys(message, EVENT_DROPPED_ITEM_RESOLVED_KEYS)
1355
+ return {
1356
+ type: 'dropped_item_resolved',
1357
+ id: id(message['id']),
1358
+ window_id: id(message['window_id']),
1359
+ target_id: id(message['target_id']),
1360
+ drop_id: id(message['drop_id']),
1361
+ item: parseDropItemIndex(message['item']),
1362
+ root: parseRootId(message['root']),
1363
+ path: parseFilePath(message['path']),
1364
+ }
1365
+ case 'drop_target_drag':
1366
+ keys(message, EVENT_DROP_TARGET_DRAG_KEYS)
1367
+ return {
1368
+ type: 'drop_target_drag',
1369
+ sequence: id(message['sequence']),
1370
+ window_id: id(message['window_id']),
1371
+ target_id: id(message['target_id']),
1372
+ state: parseDragState(message['state']),
1373
+ }
1374
+ case 'drop_received':
1375
+ keys(message, EVENT_DROP_RECEIVED_KEYS)
1376
+ return {
1377
+ type: 'drop_received',
1378
+ sequence: id(message['sequence']),
1379
+ window_id: id(message['window_id']),
1380
+ target_id: id(message['target_id']),
1381
+ drop_id: id(message['drop_id']),
1382
+ items: parseDroppedItems(message['items']),
1383
+ }
1384
+ case 'drop_rejected':
1385
+ keys(message, EVENT_DROP_REJECTED_KEYS)
1386
+ return {
1387
+ type: 'drop_rejected',
1388
+ sequence: id(message['sequence']),
1389
+ window_id: id(message['window_id']),
1390
+ target_id: id(message['target_id']),
1391
+ reason: parseDropRejection(message['reason']),
1392
+ }
1393
+ case 'click':
1394
+ case 'focus':
1395
+ case 'blur':
1396
+ keys(message, EVENT_ROUTE_KEYS)
1397
+ return {
1398
+ type: message['type'],
1399
+ ...route(message),
1400
+ }
1401
+ case 'key_down':
1402
+ case 'key_up': {
1403
+ keys(message, EVENT_KEY_KEYS)
1404
+ const key = string(message['key'], 'key', 256)
1405
+ if (!key) throw new ProtocolError('Native key must be nonempty')
1406
+ return {
1407
+ type: message['type'],
1408
+ ...route(message),
1409
+ key,
1410
+ ...('key_char' in message
1411
+ ? { key_char: string(message['key_char'], 'key_char', 256) }
1412
+ : {}),
1413
+ modifiers: modifiers(message['modifiers']),
1414
+ }
1415
+ }
1416
+ case 'pointer_down':
1417
+ case 'pointer_up': {
1418
+ keys(message, EVENT_POINTER_KEYS)
1419
+ const button = (['left', 'right', 'middle', 'back', 'forward'] as const).find(
1420
+ (button) => button === message['button']
1421
+ )
1422
+ if (!button) throw new ProtocolError('Unknown native pointer button')
1423
+ return {
1424
+ type: message['type'],
1425
+ ...route(message),
1426
+ button,
1427
+ x: number(message['x'], 'pointer x', -1_048_576, 1_048_576),
1428
+ y: number(message['y'], 'pointer y', -1_048_576, 1_048_576),
1429
+ modifiers: modifiers(message['modifiers']),
1430
+ }
1431
+ }
1432
+ case 'dismiss':
1433
+ keys(message, EVENT_DISMISS_KEYS)
1434
+ if (message['reason'] !== 'window_blur')
1435
+ throw new ProtocolError('Unknown native dismissal reason')
1436
+ return { type: 'dismiss', ...route(message), reason: 'window_blur' }
1437
+ case 'split_resize': {
1438
+ keys(message, EVENT_SPLIT_RESIZE_KEYS)
1439
+ const source = (['pointer', 'keyboard'] as const).find(
1440
+ (source) => source === message['source']
1441
+ )
1442
+ if (!source) throw new ProtocolError('Unknown split resize source')
1443
+ return {
1444
+ type: 'split_resize',
1445
+ ...route(message),
1446
+ ratio: number(message['ratio'], 'split ratio', 0, 1),
1447
+ source,
1448
+ }
1449
+ }
1450
+ case 'closed':
1451
+ keys(message, EVENT_CLOSED_KEYS)
1452
+ return {
1453
+ type: 'closed',
1454
+ sequence: id(message['sequence']),
1455
+ window_id: id(message['window_id']),
1456
+ }
1457
+ case 'input':
1458
+ keys(message, EVENT_INPUT_KEYS)
1459
+ return {
1460
+ type: 'input',
1461
+ ...route(message),
1462
+ value: string(message['value'], 'value'),
1463
+ input_revision: integer(message['input_revision'], 'input revision'),
1464
+ }
1465
+ case 'app_menu_set':
1466
+ keys(message, EVENT_APP_MENU_SET_KEYS)
1467
+ return { type: 'app_menu_set', id: id(message['id']), menu_id: id(message['menu_id']) }
1468
+ case 'app_menu_item':
1469
+ keys(message, EVENT_APP_MENU_ITEM_KEYS)
1470
+ return {
1471
+ type: 'app_menu_item',
1472
+ sequence: id(message['sequence']),
1473
+ menu_id: id(message['menu_id']),
1474
+ item_id: parseAppMenuItemId(message['item_id']),
1475
+ }
1476
+ case 'error': {
1477
+ keys(message, EVENT_ERROR_KEYS)
1478
+ const code = ERROR_CODES.find((code) => code === message['code'])
1479
+ if (!code) throw new ProtocolError('Invalid native error code')
1480
+ return {
1481
+ type: 'error',
1482
+ code,
1483
+ message: string(message['message'], 'message'),
1484
+ ...('id' in message ? { id: id(message['id']) } : {}),
1485
+ }
1486
+ }
1487
+ default:
1488
+ throw new ProtocolError('Unknown native event')
1489
+ }
1490
+ }