@bun-native/protocol 0.33.0 → 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.
- package/README.md +1 -1
- package/bun-native-source.json +30 -30
- package/package.json +1 -1
- package/src/app-menu.ts +20 -12
- package/src/dialogs.ts +21 -6
- package/src/drops.ts +4 -2
- package/src/elements.ts +76 -47
- package/src/files.ts +26 -12
- package/src/framing.ts +33 -9
- package/src/index.ts +10 -24
- package/src/message-dialogs.ts +22 -16
- package/src/messages.ts +181 -79
- package/src/processes.ts +19 -8
- package/src/services.ts +30 -19
- package/src/values.ts +8 -2
package/README.md
CHANGED
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
|
|
3
3
|
The private native protocol shared by Bun Native packages and hosts.
|
|
4
4
|
|
|
5
|
-
Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.
|
|
5
|
+
Part of [Bun Native](https://github.com/0xPlayerOne/bun-native), an experimental developer preview. Version 0.33.1 was built from [c56fdd675365](https://github.com/0xPlayerOne/bun-native/tree/c56fdd6753653a10144a4167c1d3dfb3c38a0647) and published from GitHub Actions with npm provenance. Use the Bun revision pinned in that commit's toolchain.json. Start with the [getting-started guide](https://github.com/0xPlayerOne/bun-native/blob/c56fdd6753653a10144a4167c1d3dfb3c38a0647/docs/getting-started.md); `bun-native host install` fetches the matching release host. The license notice is in LICENSE.
|
package/bun-native-source.json
CHANGED
|
@@ -1,63 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"commit": "
|
|
4
|
-
"tree": "
|
|
3
|
+
"commit": "c56fdd6753653a10144a4167c1d3dfb3c38a0647",
|
|
4
|
+
"tree": "c6e81b3f9d041e11dd0413b5fdd7a50910383854",
|
|
5
5
|
"files": {
|
|
6
6
|
"package.json": {
|
|
7
|
-
"bytes":
|
|
8
|
-
"sha256": "
|
|
7
|
+
"bytes": 2115,
|
|
8
|
+
"sha256": "caf866d56f3cd7530d5735ecbf62512e9e22526bc44c75a4b48a18c26352c0b8"
|
|
9
9
|
},
|
|
10
10
|
"packages/protocol/package.json": {
|
|
11
11
|
"bytes": 129,
|
|
12
12
|
"sha256": "668f52c10a6db801c2d0c68c59998c489b50c0bb79cb7f60298dfbb83923b0ba"
|
|
13
13
|
},
|
|
14
14
|
"packages/protocol/src/app-menu.ts": {
|
|
15
|
-
"bytes":
|
|
16
|
-
"sha256": "
|
|
15
|
+
"bytes": 6687,
|
|
16
|
+
"sha256": "a0346cff0bfe26a1b2d1eaf1470521d5602d59dd54f831b3353be56499a5efc3"
|
|
17
17
|
},
|
|
18
18
|
"packages/protocol/src/dialogs.ts": {
|
|
19
|
-
"bytes":
|
|
20
|
-
"sha256": "
|
|
19
|
+
"bytes": 5261,
|
|
20
|
+
"sha256": "c26aab88e0e6ca9838b8c7dd471165e03f967694576a257dec88cff8343e4175"
|
|
21
21
|
},
|
|
22
22
|
"packages/protocol/src/drops.ts": {
|
|
23
|
-
"bytes":
|
|
24
|
-
"sha256": "
|
|
23
|
+
"bytes": 2029,
|
|
24
|
+
"sha256": "03894f9b2b40f7645e74824de630bebc4d7d7145eafade79739248126826a42b"
|
|
25
25
|
},
|
|
26
26
|
"packages/protocol/src/elements.ts": {
|
|
27
|
-
"bytes":
|
|
28
|
-
"sha256": "
|
|
27
|
+
"bytes": 17017,
|
|
28
|
+
"sha256": "b831c4906806cd8b46aab29983481c85617b84b4bcc6cde515c3c2d8f5f4dfb1"
|
|
29
29
|
},
|
|
30
30
|
"packages/protocol/src/files.ts": {
|
|
31
|
-
"bytes":
|
|
32
|
-
"sha256": "
|
|
31
|
+
"bytes": 10818,
|
|
32
|
+
"sha256": "4fda4c2ebcc489f330b52ff042fe3b005b7b09718799f4b684d06c7728b86730"
|
|
33
33
|
},
|
|
34
34
|
"packages/protocol/src/framing.ts": {
|
|
35
|
-
"bytes":
|
|
36
|
-
"sha256": "
|
|
35
|
+
"bytes": 4321,
|
|
36
|
+
"sha256": "63148dfde50d4e98dafd8e1923db43d290565453a28113cf356ed3e33c4a5d70"
|
|
37
37
|
},
|
|
38
38
|
"packages/protocol/src/index.ts": {
|
|
39
|
-
"bytes":
|
|
40
|
-
"sha256": "
|
|
39
|
+
"bytes": 2940,
|
|
40
|
+
"sha256": "3aa7a5bb90bbcb4c58fa999a6179fcca860b53913984efb857505d90438e79ae"
|
|
41
41
|
},
|
|
42
42
|
"packages/protocol/src/message-dialogs.ts": {
|
|
43
|
-
"bytes":
|
|
44
|
-
"sha256": "
|
|
43
|
+
"bytes": 4229,
|
|
44
|
+
"sha256": "3fe42b3c42ff255761a867c87d0ea69f0404ee9dd64ba57cda9f9573588db210"
|
|
45
45
|
},
|
|
46
46
|
"packages/protocol/src/messages.ts": {
|
|
47
|
-
"bytes":
|
|
48
|
-
"sha256": "
|
|
47
|
+
"bytes": 48950,
|
|
48
|
+
"sha256": "17d92d1b5f08fb04f1dc9e6269291ffc7ff39f3c6ace3c9a6b7b216c6a63c2d3"
|
|
49
49
|
},
|
|
50
50
|
"packages/protocol/src/processes.ts": {
|
|
51
|
-
"bytes":
|
|
52
|
-
"sha256": "
|
|
51
|
+
"bytes": 10019,
|
|
52
|
+
"sha256": "b64ee81b209a4e6ccf0965409c355a1bc3bfa8d813668aec385b50050be4f2b6"
|
|
53
53
|
},
|
|
54
54
|
"packages/protocol/src/services.ts": {
|
|
55
|
-
"bytes":
|
|
56
|
-
"sha256": "
|
|
55
|
+
"bytes": 7933,
|
|
56
|
+
"sha256": "4906b13929e96382fe55c8f94316e5b6629c74a6482ce25c55c621bc449a45e9"
|
|
57
57
|
},
|
|
58
58
|
"packages/protocol/src/values.ts": {
|
|
59
|
-
"bytes":
|
|
60
|
-
"sha256": "
|
|
59
|
+
"bytes": 2365,
|
|
60
|
+
"sha256": "cca3d96f72dde820f154b6e492ea1a94ee33d8af8da44aca6fd1039912de63c1"
|
|
61
61
|
},
|
|
62
62
|
"LICENSE": {
|
|
63
63
|
"bytes": 1075,
|
|
@@ -65,8 +65,8 @@
|
|
|
65
65
|
}
|
|
66
66
|
},
|
|
67
67
|
"toolchain": {
|
|
68
|
-
"bytes":
|
|
69
|
-
"sha256": "
|
|
68
|
+
"bytes": 3234,
|
|
69
|
+
"sha256": "9f4d9362cb4cc38c0f4e705295789a0cbfec46ffdd007e0541b9441a7c3003a7"
|
|
70
70
|
},
|
|
71
71
|
"dependencyLock": {
|
|
72
72
|
"bytes": 28357,
|
package/package.json
CHANGED
package/src/app-menu.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { ProtocolError } from './framing.ts'
|
|
2
|
-
import { keys, object, string } from './values.ts'
|
|
2
|
+
import { fields, keys, object, string } from './values.ts'
|
|
3
3
|
|
|
4
4
|
/** Top-level menus in one menu bar. */
|
|
5
|
-
|
|
5
|
+
const MAX_APP_MENUS = 16
|
|
6
6
|
/** Items, separators and submenus in one menu bar, at every depth. */
|
|
7
7
|
export const MAX_APP_MENU_ENTRIES = 128
|
|
8
8
|
/** A top-level menu is depth 1; submenus nest at most two levels below it. */
|
|
9
9
|
export const MAX_APP_MENU_DEPTH = 3
|
|
10
|
-
|
|
11
|
-
export const MAX_APP_MENU_ITEM_ID_BYTES = 64
|
|
10
|
+
const MAX_APP_MENU_LABEL_BYTES = 64
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
const APP_MENU_NAMED_KEYS = Object.freeze([
|
|
14
13
|
'enter',
|
|
15
14
|
'escape',
|
|
16
15
|
'tab',
|
|
@@ -26,7 +25,7 @@ export const APP_MENU_NAMED_KEYS = Object.freeze([
|
|
|
26
25
|
'pageup',
|
|
27
26
|
'pagedown',
|
|
28
27
|
] as const)
|
|
29
|
-
|
|
28
|
+
const APP_MENU_PUNCTUATION_KEYS = Object.freeze([
|
|
30
29
|
',',
|
|
31
30
|
'.',
|
|
32
31
|
'/',
|
|
@@ -104,14 +103,17 @@ function flag(data: Record<string, unknown>, key: string): boolean {
|
|
|
104
103
|
|
|
105
104
|
const isFunctionKey = (key: string) => /^f([1-9]|1[0-2])$/.test(key)
|
|
106
105
|
|
|
106
|
+
const MENU_SHORTCUT_KEYS = fields('key', 'modifiers')
|
|
107
|
+
const MENU_MODIFIER_KEYS = fields('control', 'alt', 'shift', 'platform')
|
|
108
|
+
|
|
107
109
|
function parseMenuShortcut(value: unknown): MenuShortcut {
|
|
108
110
|
const data = object(value)
|
|
109
|
-
keys(data,
|
|
111
|
+
keys(data, MENU_SHORTCUT_KEYS)
|
|
110
112
|
const key = data['key']
|
|
111
113
|
if (typeof key !== 'string' || !APP_MENU_KEYS.includes(key))
|
|
112
114
|
throw new ProtocolError('Menu shortcut key is not in the closed key set')
|
|
113
115
|
const raw = object(data['modifiers'])
|
|
114
|
-
keys(raw,
|
|
116
|
+
keys(raw, MENU_MODIFIER_KEYS)
|
|
115
117
|
const modifiers = {
|
|
116
118
|
control: flag(raw, 'control'),
|
|
117
119
|
alt: flag(raw, 'alt'),
|
|
@@ -132,6 +134,10 @@ interface Census {
|
|
|
132
134
|
shortcuts: Set<string>
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
const ENTRIES_ITEM_KEYS = fields('kind', 'id', 'label', 'shortcut', 'enabled', 'checked')
|
|
138
|
+
const ENTRIES_SEPARATOR_KEYS = fields('kind')
|
|
139
|
+
const ENTRIES_SUBMENU_KEYS = fields('kind', 'label', 'enabled', 'items')
|
|
140
|
+
|
|
135
141
|
function entries(value: unknown, depth: number, census: Census): AppMenuEntry[] {
|
|
136
142
|
if (!Array.isArray(value) || !value.length)
|
|
137
143
|
throw new ProtocolError('A menu or submenu needs at least one entry')
|
|
@@ -141,7 +147,7 @@ function entries(value: unknown, depth: number, census: Census): AppMenuEntry[]
|
|
|
141
147
|
const data = object(raw)
|
|
142
148
|
switch (data['kind']) {
|
|
143
149
|
case 'item': {
|
|
144
|
-
keys(data,
|
|
150
|
+
keys(data, ENTRIES_ITEM_KEYS)
|
|
145
151
|
const id = parseAppMenuItemId(data['id'])
|
|
146
152
|
if (census.ids.has(id))
|
|
147
153
|
throw new ProtocolError('Menu item ids must be unique in a menu bar')
|
|
@@ -164,10 +170,10 @@ function entries(value: unknown, depth: number, census: Census): AppMenuEntry[]
|
|
|
164
170
|
return entry
|
|
165
171
|
}
|
|
166
172
|
case 'separator':
|
|
167
|
-
keys(data,
|
|
173
|
+
keys(data, ENTRIES_SEPARATOR_KEYS)
|
|
168
174
|
return { kind: 'separator' }
|
|
169
175
|
case 'submenu': {
|
|
170
|
-
keys(data,
|
|
176
|
+
keys(data, ENTRIES_SUBMENU_KEYS)
|
|
171
177
|
const text = label(data['label'], 'menu label')
|
|
172
178
|
const enabled = flag(data, 'enabled')
|
|
173
179
|
if (depth >= MAX_APP_MENU_DEPTH)
|
|
@@ -193,6 +199,8 @@ export function parseContextMenuItems(value: unknown): AppMenuEntry[] {
|
|
|
193
199
|
return entries(value, 1, { entries: 0, ids: new Set(), shortcuts: new Set() })
|
|
194
200
|
}
|
|
195
201
|
|
|
202
|
+
const APP_MENUS_KEYS = fields('title', 'items')
|
|
203
|
+
|
|
196
204
|
/** An empty bar is valid: it removes the application's menus. */
|
|
197
205
|
export function parseAppMenus(value: unknown): AppMenu[] {
|
|
198
206
|
if (!Array.isArray(value) || value.length > MAX_APP_MENUS)
|
|
@@ -200,7 +208,7 @@ export function parseAppMenus(value: unknown): AppMenu[] {
|
|
|
200
208
|
const census: Census = { entries: 0, ids: new Set(), shortcuts: new Set() }
|
|
201
209
|
return value.map((raw) => {
|
|
202
210
|
const data = object(raw)
|
|
203
|
-
keys(data,
|
|
211
|
+
keys(data, APP_MENUS_KEYS)
|
|
204
212
|
return {
|
|
205
213
|
title: label(data['title'], 'menu title'),
|
|
206
214
|
items: entries(data['items'], 1, census),
|
package/src/dialogs.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProtocolError } from './framing.ts'
|
|
2
|
-
import { keys, object, string } from './values.ts'
|
|
2
|
+
import { fields, keys, object, string } from './values.ts'
|
|
3
3
|
|
|
4
4
|
/** Live file dialogs per session. Each window owns at most one at a time. */
|
|
5
5
|
export const MAX_FILE_DIALOGS = 16
|
|
@@ -56,9 +56,11 @@ function parseDialogPath(value: unknown, field = 'dialog path'): string {
|
|
|
56
56
|
return path
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
const FILTER_KEYS = fields('name', 'patterns')
|
|
60
|
+
|
|
59
61
|
function filter(value: unknown): FileFilter {
|
|
60
62
|
const data = object(value)
|
|
61
|
-
keys(data,
|
|
63
|
+
keys(data, FILTER_KEYS)
|
|
62
64
|
const patterns = data['patterns']
|
|
63
65
|
if (
|
|
64
66
|
!Array.isArray(patterns) ||
|
|
@@ -74,9 +76,18 @@ function filter(value: unknown): FileFilter {
|
|
|
74
76
|
}
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
const FILE_DIALOG_OPTIONS_KEYS = fields(
|
|
80
|
+
'mode',
|
|
81
|
+
'title',
|
|
82
|
+
'accept_label',
|
|
83
|
+
'folder',
|
|
84
|
+
'name',
|
|
85
|
+
'filters'
|
|
86
|
+
)
|
|
87
|
+
|
|
77
88
|
export function parseFileDialogOptions(value: unknown): FileDialogOptions {
|
|
78
89
|
const data = object(value)
|
|
79
|
-
keys(data,
|
|
90
|
+
keys(data, FILE_DIALOG_OPTIONS_KEYS)
|
|
80
91
|
const mode = FILE_DIALOG_MODES.find((candidate) => candidate === data['mode'])
|
|
81
92
|
if (!mode) throw new ProtocolError('Unknown file dialog mode')
|
|
82
93
|
const options: FileDialogOptions = {
|
|
@@ -104,21 +115,25 @@ export function parseFileDialogOptions(value: unknown): FileDialogOptions {
|
|
|
104
115
|
return options
|
|
105
116
|
}
|
|
106
117
|
|
|
118
|
+
const FILE_DIALOG_RESULT_SELECTED_KEYS = fields('kind', 'paths')
|
|
119
|
+
const FILE_DIALOG_RESULT_CANCELLED_KEYS = fields('kind')
|
|
120
|
+
const FILE_DIALOG_RESULT_FAILED_KEYS = fields('kind', 'code')
|
|
121
|
+
|
|
107
122
|
export function parseFileDialogResult(value: unknown): FileDialogResult {
|
|
108
123
|
const data = object(value)
|
|
109
124
|
switch (data['kind']) {
|
|
110
125
|
case 'selected': {
|
|
111
|
-
keys(data,
|
|
126
|
+
keys(data, FILE_DIALOG_RESULT_SELECTED_KEYS)
|
|
112
127
|
const paths = data['paths']
|
|
113
128
|
if (!Array.isArray(paths) || paths.length < 1 || paths.length > MAX_DIALOG_PATHS)
|
|
114
129
|
throw new ProtocolError('A dialog selection holds between 1 and 256 paths')
|
|
115
130
|
return { kind: 'selected', paths: paths.map((path) => parseDialogPath(path)) }
|
|
116
131
|
}
|
|
117
132
|
case 'cancelled':
|
|
118
|
-
keys(data,
|
|
133
|
+
keys(data, FILE_DIALOG_RESULT_CANCELLED_KEYS)
|
|
119
134
|
return { kind: 'cancelled' }
|
|
120
135
|
case 'failed': {
|
|
121
|
-
keys(data,
|
|
136
|
+
keys(data, FILE_DIALOG_RESULT_FAILED_KEYS)
|
|
122
137
|
const code = (['native_failure', 'payload_too_large'] as const).find(
|
|
123
138
|
(candidate) => candidate === data['code']
|
|
124
139
|
)
|
package/src/drops.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProtocolError } from './framing.ts'
|
|
2
|
-
import { integer, keys, object, string } from './values.ts'
|
|
2
|
+
import { fields, integer, keys, object, string } from './values.ts'
|
|
3
3
|
|
|
4
4
|
/** Live drop targets across a session. */
|
|
5
5
|
export const MAX_DROP_TARGETS = 32
|
|
@@ -33,12 +33,14 @@ export function parseDropItemIndex(value: unknown): number {
|
|
|
33
33
|
return integer(value, 'Dropped item index', 0, MAX_DROP_ITEMS - 1)
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
const DROPPED_ITEMS_KEYS = fields('name')
|
|
37
|
+
|
|
36
38
|
export function parseDroppedItems(value: unknown): DroppedItem[] {
|
|
37
39
|
if (!Array.isArray(value) || value.length < 1 || value.length > MAX_DROP_ITEMS)
|
|
38
40
|
throw new ProtocolError('A drop must deliver between 1 and ' + MAX_DROP_ITEMS + ' items')
|
|
39
41
|
return value.map((entry) => {
|
|
40
42
|
const data = object(entry)
|
|
41
|
-
keys(data,
|
|
43
|
+
keys(data, DROPPED_ITEMS_KEYS)
|
|
42
44
|
const name = string(data['name'], 'Dropped item name', MAX_DROP_NAME_BYTES)
|
|
43
45
|
if (!name || /\p{Cc}/u.test(name))
|
|
44
46
|
throw new ProtocolError('Dropped item name must be nonempty without control characters')
|
package/src/elements.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProtocolError } from './framing.ts'
|
|
2
|
-
import { integer, keys, number, object, string } from './values.ts'
|
|
2
|
+
import { fields, integer, keys, number, object, string } from './values.ts'
|
|
3
3
|
|
|
4
4
|
export interface Style {
|
|
5
5
|
direction?: 'row' | 'column'
|
|
@@ -104,7 +104,7 @@ export interface AccessibleLabel {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
/** The largest `accessible_set_size` and `accessible_position`. */
|
|
107
|
-
|
|
107
|
+
const MAX_ACCESSIBLE_SET_SIZE = 2_147_483_647
|
|
108
108
|
|
|
109
109
|
/**
|
|
110
110
|
* Requires `accessibility_lists_v1`. A box without `accessible_role` is layout
|
|
@@ -165,29 +165,31 @@ function requireLeafStyle(style: Style): asserts style is LeafStyle {
|
|
|
165
165
|
throw new ProtocolError('Scrolling is supported only on box and overlay elements')
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
+
const STYLE_KEYS = fields(
|
|
169
|
+
'direction',
|
|
170
|
+
'gap',
|
|
171
|
+
'padding',
|
|
172
|
+
'width',
|
|
173
|
+
'height',
|
|
174
|
+
'min_width',
|
|
175
|
+
'max_width',
|
|
176
|
+
'min_height',
|
|
177
|
+
'max_height',
|
|
178
|
+
'flex_grow',
|
|
179
|
+
'flex_shrink',
|
|
180
|
+
'align_items',
|
|
181
|
+
'justify_content',
|
|
182
|
+
'overflow',
|
|
183
|
+
'scroll_anchor',
|
|
184
|
+
'background',
|
|
185
|
+
'color',
|
|
186
|
+
'font_size',
|
|
187
|
+
...DECORATION_FIELDS
|
|
188
|
+
)
|
|
189
|
+
|
|
168
190
|
function parseStyle(value: unknown): Style {
|
|
169
191
|
const data = object(value)
|
|
170
|
-
keys(data,
|
|
171
|
-
'direction',
|
|
172
|
-
'gap',
|
|
173
|
-
'padding',
|
|
174
|
-
'width',
|
|
175
|
-
'height',
|
|
176
|
-
'min_width',
|
|
177
|
-
'max_width',
|
|
178
|
-
'min_height',
|
|
179
|
-
'max_height',
|
|
180
|
-
'flex_grow',
|
|
181
|
-
'flex_shrink',
|
|
182
|
-
'align_items',
|
|
183
|
-
'justify_content',
|
|
184
|
-
'overflow',
|
|
185
|
-
'scroll_anchor',
|
|
186
|
-
'background',
|
|
187
|
-
'color',
|
|
188
|
-
'font_size',
|
|
189
|
-
...DECORATION_FIELDS,
|
|
190
|
-
])
|
|
192
|
+
keys(data, STYLE_KEYS)
|
|
191
193
|
const style: Style = {}
|
|
192
194
|
for (const key of ['gap', 'padding'] as const)
|
|
193
195
|
if (key in data) style[key] = number(data[key], key, 0, 4_096)
|
|
@@ -310,24 +312,60 @@ function boxAccessibility(element: Record<string, unknown>): BoxAccessibility {
|
|
|
310
312
|
}
|
|
311
313
|
}
|
|
312
314
|
|
|
313
|
-
|
|
315
|
+
const ELEMENT_BOX_KEYS = fields(
|
|
316
|
+
'kind',
|
|
317
|
+
'accessible_role',
|
|
318
|
+
'accessible_label',
|
|
319
|
+
'accessible_set_size',
|
|
320
|
+
'accessible_position',
|
|
321
|
+
'accessible_selected',
|
|
322
|
+
'style'
|
|
323
|
+
)
|
|
324
|
+
const ELEMENT_TEXT_KEYS = fields('kind', 'text', 'accessible_live', 'style')
|
|
325
|
+
const ELEMENT_BUTTON_KEYS = fields('kind', 'text', 'disabled', 'style')
|
|
326
|
+
const ELEMENT_INPUT_KEYS = fields('kind', 'value', 'placeholder', 'disabled', 'read_only', 'style')
|
|
327
|
+
const ELEMENT_SPLIT_KEYS = fields('kind', 'ratio', 'min_size', 'accessible_label', 'style')
|
|
328
|
+
const ELEMENT_OVERLAY_KEYS = fields(
|
|
329
|
+
'kind',
|
|
330
|
+
'anchor',
|
|
331
|
+
'modal',
|
|
332
|
+
'surface',
|
|
333
|
+
'autofocus',
|
|
334
|
+
'navigation',
|
|
335
|
+
'accessible_label',
|
|
336
|
+
'style'
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Elements this module produced. Each is frozen with its style, so it is still
|
|
341
|
+
* exactly the normalized value that passed validation.
|
|
342
|
+
*/
|
|
343
|
+
const parsed = new WeakSet<Element>()
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Normalize and validate the entire property set, including JavaScript consumers.
|
|
347
|
+
*
|
|
348
|
+
* The result is frozen and returned unchanged when parsed again, so core's
|
|
349
|
+
* normalized elements cross the runtime boundary in O(1). Any other value,
|
|
350
|
+
* including an unfrozen copy of a parsed element, is validated in full.
|
|
351
|
+
*/
|
|
314
352
|
export function parseElement(value: unknown): Element {
|
|
353
|
+
if (parsed.has(value as Element)) return value as Element
|
|
354
|
+
const element = validElement(value)
|
|
355
|
+
Object.freeze(element.style)
|
|
356
|
+
parsed.add(Object.freeze(element))
|
|
357
|
+
return element
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function validElement(value: unknown): Element {
|
|
315
361
|
const element = object(value)
|
|
316
362
|
const style = 'style' in element ? parseStyle(element['style']) : {}
|
|
317
363
|
switch (element['kind']) {
|
|
318
364
|
case 'box':
|
|
319
|
-
keys(element,
|
|
320
|
-
'kind',
|
|
321
|
-
'accessible_role',
|
|
322
|
-
'accessible_label',
|
|
323
|
-
'accessible_set_size',
|
|
324
|
-
'accessible_position',
|
|
325
|
-
'accessible_selected',
|
|
326
|
-
'style',
|
|
327
|
-
])
|
|
365
|
+
keys(element, ELEMENT_BOX_KEYS)
|
|
328
366
|
return { kind: 'box', ...boxAccessibility(element), style }
|
|
329
367
|
case 'text':
|
|
330
|
-
keys(element,
|
|
368
|
+
keys(element, ELEMENT_TEXT_KEYS)
|
|
331
369
|
requireLeafStyle(style)
|
|
332
370
|
return {
|
|
333
371
|
kind: 'text',
|
|
@@ -343,7 +381,7 @@ export function parseElement(value: unknown): Element {
|
|
|
343
381
|
style,
|
|
344
382
|
}
|
|
345
383
|
case 'button': {
|
|
346
|
-
keys(element,
|
|
384
|
+
keys(element, ELEMENT_BUTTON_KEYS)
|
|
347
385
|
requireLeafStyle(style)
|
|
348
386
|
if ('disabled' in element && typeof element['disabled'] !== 'boolean')
|
|
349
387
|
throw new ProtocolError('disabled must be a boolean')
|
|
@@ -355,7 +393,7 @@ export function parseElement(value: unknown): Element {
|
|
|
355
393
|
}
|
|
356
394
|
}
|
|
357
395
|
case 'input': {
|
|
358
|
-
keys(element,
|
|
396
|
+
keys(element, ELEMENT_INPUT_KEYS)
|
|
359
397
|
requireLeafStyle(style)
|
|
360
398
|
for (const name of ['disabled', 'read_only'])
|
|
361
399
|
if (name in element && typeof element[name] !== 'boolean')
|
|
@@ -371,7 +409,7 @@ export function parseElement(value: unknown): Element {
|
|
|
371
409
|
}
|
|
372
410
|
}
|
|
373
411
|
case 'split': {
|
|
374
|
-
keys(element,
|
|
412
|
+
keys(element, ELEMENT_SPLIT_KEYS)
|
|
375
413
|
requireLeafStyle(style)
|
|
376
414
|
if (
|
|
377
415
|
style.gap !== undefined ||
|
|
@@ -388,16 +426,7 @@ export function parseElement(value: unknown): Element {
|
|
|
388
426
|
}
|
|
389
427
|
}
|
|
390
428
|
case 'overlay': {
|
|
391
|
-
keys(element,
|
|
392
|
-
'kind',
|
|
393
|
-
'anchor',
|
|
394
|
-
'modal',
|
|
395
|
-
'surface',
|
|
396
|
-
'autofocus',
|
|
397
|
-
'navigation',
|
|
398
|
-
'accessible_label',
|
|
399
|
-
'style',
|
|
400
|
-
])
|
|
429
|
+
keys(element, ELEMENT_OVERLAY_KEYS)
|
|
401
430
|
const label = accessibleLabel(element)
|
|
402
431
|
const anchor =
|
|
403
432
|
'anchor' in element ? choice(element['anchor'], 'anchor', ['window', 'parent']) : 'window'
|
package/src/files.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ProtocolError } from './framing.ts'
|
|
2
|
-
import { integer, keys, object, string } from './values.ts'
|
|
2
|
+
import { fields, integer, keys, object, string } from './values.ts'
|
|
3
3
|
|
|
4
4
|
export const MAX_FILESYSTEM_ROOTS = 8
|
|
5
5
|
export const MAX_FILE_GRANTS = 32
|
|
@@ -87,9 +87,11 @@ export function parseFileAccess(value: unknown): FileAccess {
|
|
|
87
87
|
return value as FileAccess
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
const ROOT_KEYS = fields('id', 'path', 'access')
|
|
91
|
+
|
|
90
92
|
function root(value: unknown): FilesystemRoot {
|
|
91
93
|
const data = object(value)
|
|
92
|
-
keys(data,
|
|
94
|
+
keys(data, ROOT_KEYS)
|
|
93
95
|
return {
|
|
94
96
|
id: parseRootId(data['id']),
|
|
95
97
|
path: parseRootPath(data['path']),
|
|
@@ -158,6 +160,11 @@ export function parseChunk(value: unknown): string {
|
|
|
158
160
|
const WRITE_MODES: readonly WriteMode[] = ['create', 'truncate', 'append']
|
|
159
161
|
const FILE_KINDS: readonly FileKind[] = ['file', 'directory', 'symlink', 'other']
|
|
160
162
|
|
|
163
|
+
const FILE_OPERATION_STAT_KEYS = fields('kind')
|
|
164
|
+
const FILE_OPERATION_LIST_KEYS = fields('kind', 'after')
|
|
165
|
+
const FILE_OPERATION_READ_KEYS = fields('kind', 'offset', 'length')
|
|
166
|
+
const FILE_OPERATION_WRITE_KEYS = fields('kind', 'mode', 'data')
|
|
167
|
+
|
|
161
168
|
export function parseFileOperation(value: unknown, path: string): FileOperation {
|
|
162
169
|
const data = object(value)
|
|
163
170
|
const writes = ['write', 'create_directory', 'remove'].includes(data['kind'] as string)
|
|
@@ -167,15 +174,15 @@ export function parseFileOperation(value: unknown, path: string): FileOperation
|
|
|
167
174
|
case 'stat':
|
|
168
175
|
case 'create_directory':
|
|
169
176
|
case 'remove':
|
|
170
|
-
keys(data,
|
|
177
|
+
keys(data, FILE_OPERATION_STAT_KEYS)
|
|
171
178
|
return { kind: data['kind'] }
|
|
172
179
|
case 'list':
|
|
173
|
-
keys(data,
|
|
180
|
+
keys(data, FILE_OPERATION_LIST_KEYS)
|
|
174
181
|
return 'after' in data
|
|
175
182
|
? { kind: 'list', after: parseFileName(data['after']) }
|
|
176
183
|
: { kind: 'list' }
|
|
177
184
|
case 'read':
|
|
178
|
-
keys(data,
|
|
185
|
+
keys(data, FILE_OPERATION_READ_KEYS)
|
|
179
186
|
return {
|
|
180
187
|
kind: 'read',
|
|
181
188
|
offset: integer(
|
|
@@ -187,7 +194,7 @@ export function parseFileOperation(value: unknown, path: string): FileOperation
|
|
|
187
194
|
length: integer(data['length'], 'file read length', 1, MAX_FILE_CHUNK_BYTES),
|
|
188
195
|
}
|
|
189
196
|
case 'write': {
|
|
190
|
-
keys(data,
|
|
197
|
+
keys(data, FILE_OPERATION_WRITE_KEYS)
|
|
191
198
|
const mode = WRITE_MODES.find((item) => item === data['mode'])
|
|
192
199
|
if (!mode) throw new ProtocolError('Unknown file write mode')
|
|
193
200
|
return { kind: 'write', mode, data: parseChunk(data['data']) }
|
|
@@ -211,11 +218,18 @@ function compareBytes(left: string, right: string): number {
|
|
|
211
218
|
return a.length - b.length
|
|
212
219
|
}
|
|
213
220
|
|
|
221
|
+
const FILE_RESULT_STAT_KEYS = fields('kind', 'file_kind', 'size', 'modified_ms')
|
|
222
|
+
const FILE_RESULT_LIST_KEYS = fields('kind', 'entries', 'complete', 'omitted')
|
|
223
|
+
const FILE_LIST_ENTRY_KEYS = fields('name', 'kind')
|
|
224
|
+
const FILE_RESULT_READ_KEYS = fields('kind', 'data', 'end_of_file')
|
|
225
|
+
const FILE_RESULT_WRITTEN_KEYS = fields('kind', 'bytes')
|
|
226
|
+
const FILE_RESULT_DONE_KEYS = fields('kind')
|
|
227
|
+
|
|
214
228
|
export function parseFileResult(value: unknown): FileResult {
|
|
215
229
|
const data = object(value)
|
|
216
230
|
switch (data['kind']) {
|
|
217
231
|
case 'stat':
|
|
218
|
-
keys(data,
|
|
232
|
+
keys(data, FILE_RESULT_STAT_KEYS)
|
|
219
233
|
return {
|
|
220
234
|
kind: 'stat',
|
|
221
235
|
file_kind: fileKind(data['file_kind']),
|
|
@@ -227,13 +241,13 @@ export function parseFileResult(value: unknown): FileResult {
|
|
|
227
241
|
),
|
|
228
242
|
}
|
|
229
243
|
case 'list': {
|
|
230
|
-
keys(data,
|
|
244
|
+
keys(data, FILE_RESULT_LIST_KEYS)
|
|
231
245
|
const entries = data['entries']
|
|
232
246
|
if (!Array.isArray(entries) || entries.length > MAX_DIRECTORY_ENTRIES)
|
|
233
247
|
throw new ProtocolError('Directory page exceeds ' + MAX_DIRECTORY_ENTRIES + ' entries')
|
|
234
248
|
const parsed = entries.map((entry) => {
|
|
235
249
|
const item = object(entry)
|
|
236
|
-
keys(item,
|
|
250
|
+
keys(item, FILE_LIST_ENTRY_KEYS)
|
|
237
251
|
return { name: parseFileName(item['name']), kind: fileKind(item['kind']) }
|
|
238
252
|
})
|
|
239
253
|
for (let index = 1; index < parsed.length; index++)
|
|
@@ -249,18 +263,18 @@ export function parseFileResult(value: unknown): FileResult {
|
|
|
249
263
|
}
|
|
250
264
|
}
|
|
251
265
|
case 'read':
|
|
252
|
-
keys(data,
|
|
266
|
+
keys(data, FILE_RESULT_READ_KEYS)
|
|
253
267
|
if (typeof data['end_of_file'] !== 'boolean')
|
|
254
268
|
throw new ProtocolError('File end-of-file flag must be a boolean')
|
|
255
269
|
return { kind: 'read', data: parseChunk(data['data']), end_of_file: data['end_of_file'] }
|
|
256
270
|
case 'written':
|
|
257
|
-
keys(data,
|
|
271
|
+
keys(data, FILE_RESULT_WRITTEN_KEYS)
|
|
258
272
|
return {
|
|
259
273
|
kind: 'written',
|
|
260
274
|
bytes: integer(data['bytes'], 'written bytes', 0, MAX_FILE_CHUNK_BYTES),
|
|
261
275
|
}
|
|
262
276
|
case 'done':
|
|
263
|
-
keys(data,
|
|
277
|
+
keys(data, FILE_RESULT_DONE_KEYS)
|
|
264
278
|
return { kind: 'done' }
|
|
265
279
|
default:
|
|
266
280
|
throw new ProtocolError('Unknown file result')
|