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