cj-tauri 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +113 -99
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +63 -52
- package/cli/cjpm.toml +22 -22
- package/cli/src/doctor.cj +308 -306
- package/cli/src/main.cj +129 -129
- package/cli/src/manifest.cj +632 -558
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -770
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +369 -369
- package/cli/src/sync.cj +526 -526
- package/cli/src/util.cj +168 -168
- package/cli/templates/app/README.md +51 -48
- package/cli/templates/app/assets/capabilities/default.json +7 -7
- package/cli/templates/app/assets/ui/index.html +120 -118
- package/cli/templates/app/cj-tauri.toml +32 -22
- package/cli/templates/app/cjpm.toml +34 -34
- package/cli/templates/app/src/main.cj +101 -90
- package/cli/templates/app-react/README.md +56 -56
- package/cli/templates/app-react/assets/capabilities/default.json +7 -7
- package/cli/templates/app-react/assets/ui/index.html +25 -25
- package/cli/templates/app-react/assets/ui/package.json +19 -19
- package/cli/templates/app-react/assets/ui/src/App.css +67 -67
- package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
- package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
- package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-react/cj-tauri.toml +33 -23
- package/cli/templates/app-react/cjpm.toml +34 -34
- package/cli/templates/app-react/src/main.cj +91 -91
- package/cli/templates/app-vue/README.md +56 -56
- package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
- package/cli/templates/app-vue/assets/ui/index.html +25 -25
- package/cli/templates/app-vue/assets/ui/package.json +18 -18
- package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
- package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
- package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
- package/cli/templates/app-vue/cj-tauri.toml +33 -23
- package/cli/templates/app-vue/cjpm.toml +34 -34
- package/cli/templates/app-vue/src/main.cj +91 -91
- package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
- package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
- package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/README.md +84 -79
- package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
- package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
- package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
- package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
- package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
- package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
- package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
- package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
- package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
- package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -53
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -969
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -76
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +142 -142
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -607
- package/src/host_arkweb.cj +924 -778
- package/src/host_webkit.cj +663 -570
- package/src/host_webview2.cj +663 -570
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -62
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -77
- package/src/platform/platform_assets.cj +33 -33
- package/src/platform/platform_discover.cj +176 -176
- package/src/platform/platform_env.cj +29 -29
- package/src/platform/platform_feature.cj +31 -31
- package/src/platform/platform_page_source.cj +87 -87
- package/src/platform/platform_target_info.cj +32 -32
- package/src/platform_host_provider.cj +87 -87
- package/src/plugin.cj +333 -333
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +158 -158
- package/src/plugins/notification/plugin_notification.cj +154 -154
- package/src/plugins/serial/plugin_serial.cj +458 -454
- package/src/plugins/serial/serial_bridge.cj +194 -194
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +258 -258
- package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -127
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -201
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -251
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +605 -585
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/plugins/serial/serial_ohos_stub.cj +0 -61
- package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
package/src/app.cj
CHANGED
|
@@ -1,969 +1,325 @@
|
|
|
1
|
-
package cjTauri
|
|
2
|
-
|
|
3
|
-
import std.collection.*
|
|
4
|
-
import std.env.*
|
|
5
|
-
import stdx.encoding.json.*
|
|
6
|
-
import cjTauri.platform.*
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* 附加窗口的登记项(试验性 API `TauriApp.addWindow` 的装配状态)
|
|
10
|
-
*
|
|
11
|
-
* 为什么宿主在这里就建好、html 也先留着:窗口表要在 run() 之前定稿(`hostOf` 当场可用),
|
|
12
|
-
* 而原生窗口要等 run() 才创建——两者分开存,正好对上宿主自己的两段式节奏
|
|
13
|
-
* (宿主对象构造时就有、原生窗口 start() 时才建)。
|
|
14
|
-
*/
|
|
15
|
-
class ExtraWindowSpec {
|
|
16
|
-
let label: String
|
|
17
|
-
let host: WebViewHost
|
|
18
|
-
let html: String
|
|
19
|
-
|
|
20
|
-
init(label: String, host: WebViewHost, html: String) {
|
|
21
|
-
this.label = label
|
|
22
|
-
this.host = host
|
|
23
|
-
this.html = html
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* TauriApp 装配(对标 tauri::Builder / App)
|
|
29
|
-
*
|
|
30
|
-
* 用法:
|
|
31
|
-
* let app = TauriApp()
|
|
32
|
-
* app.register("greet", GreetCommand())
|
|
33
|
-
* app.addCapabilityJson(...) // 或 addCapability(cap)
|
|
34
|
-
* app.run(html) // 启动宿主并阻塞
|
|
35
|
-
*
|
|
36
|
-
* 多窗口(试验性,批次 3 的最小切片):`addWindow(label, config, html)` 登记附加窗口,
|
|
37
|
-
* run() / runUrl() 统一启动;事件按 label 路由(`emitToWindow` / `hostOf`)。
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
private
|
|
51
|
-
//
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
*
|
|
76
|
-
*/
|
|
77
|
-
public func
|
|
78
|
-
this.
|
|
79
|
-
return this
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
*
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/**
|
|
149
|
-
*
|
|
150
|
-
*
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
public func
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
*
|
|
287
|
-
*
|
|
288
|
-
*
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
*
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* 只改默认窗口某个菜单项的**启用**状态;勾选态取自应用层留存的模型
|
|
330
|
-
*
|
|
331
|
-
* 模型里没有这个 id 时抛 `CommandException`(**不**静默拿 false 顶上):静默写会抹掉
|
|
332
|
-
* 用户看到的勾选,报错至少说得清「哪个 id 在哪个窗口找不到」。
|
|
333
|
-
*/
|
|
334
|
-
public func setMenuItemEnabled(id: String, enabled: Bool): TauriApp {
|
|
335
|
-
return this.setMenuItemEnabledTo(DEFAULT_WINDOW_LABEL, id, enabled)
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
/** 指定窗口版本(多窗口):语义同 `setMenuItemEnabled` */
|
|
339
|
-
public func setMenuItemEnabledTo(label: String, id: String, enabled: Bool): TauriApp {
|
|
340
|
-
let cur = this.requireMenuItem(label, id)
|
|
341
|
-
this.applyMenuState(label, id, enabled, cur.checked)
|
|
342
|
-
return this
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
/** 只改默认窗口某个菜单项的**勾选**状态;启用态取自应用层留存的模型 */
|
|
346
|
-
public func setMenuItemChecked(id: String, checked: Bool): TauriApp {
|
|
347
|
-
return this.setMenuItemCheckedTo(DEFAULT_WINDOW_LABEL, id, checked)
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
/** 指定窗口版本(多窗口):语义同 `setMenuItemChecked` */
|
|
351
|
-
public func setMenuItemCheckedTo(label: String, id: String, checked: Bool): TauriApp {
|
|
352
|
-
let cur = this.requireMenuItem(label, id)
|
|
353
|
-
this.applyMenuState(label, id, cur.enabled, checked)
|
|
354
|
-
return this
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
/**
|
|
358
|
-
* 取「另一维状态」的当前值(从应用层留存的模型里查)
|
|
359
|
-
*
|
|
360
|
-
* 模型不存在通常意味着应用用 `WebViewHost.setMenu()` 直设了菜单、绕过了应用层——
|
|
361
|
-
* 报错里点名这条出路,比让人对着「状态怎么被抹了」猜要好。
|
|
362
|
-
*/
|
|
363
|
-
private func requireMenuItem(label: String, id: String): MenuItem {
|
|
364
|
-
match (this.menus.get(label)) {
|
|
365
|
-
case None =>
|
|
366
|
-
throw CommandException(
|
|
367
|
-
"菜单模型不存在:窗口 \"${label}\" 的菜单是用 app.setMenu() 设的吗?" +
|
|
368
|
-
"(用 WebViewHost.setMenu 直设的菜单应用层没有模型,改单项状态请用 setMenuItemStateTo)"
|
|
369
|
-
)
|
|
370
|
-
case Some(model) =>
|
|
371
|
-
match (model.findById(id)) {
|
|
372
|
-
case Some(item) => return item
|
|
373
|
-
case None => throw CommandException("菜单项不存在:\"${id}\"(窗口 \"${label}\")")
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* 写菜单项状态:平台侧 + 应用层模型同步(模型里的 item 是可变对象,见 src/menu.cj)
|
|
380
|
-
*
|
|
381
|
-
* 模型不跟上会出错:勾选项在平台上一被点就自己翻了 checked,模型若还记旧值,
|
|
382
|
-
* 下一次「只改启用」就会拿旧值去写,把刚点出来的勾选抹回去。
|
|
383
|
-
* 模型缺失只提示、不拦——平台侧照写(与 `WebViewHost.setMenuItemState` 的「找不到静默忽略」一致)。
|
|
384
|
-
*/
|
|
385
|
-
private func applyMenuState(label: String, id: String, enabled: Bool, checked: Bool): Unit {
|
|
386
|
-
match (this.menus.get(label)) {
|
|
387
|
-
case Some(model) =>
|
|
388
|
-
match (model.findById(id)) {
|
|
389
|
-
case Some(item) =>
|
|
390
|
-
item.enabled = enabled
|
|
391
|
-
item.checked = checked
|
|
392
|
-
case None => eprintln("[cj-tauri] setMenuItemState: 模型里没有 \"${id}\"(只改平台侧)")
|
|
393
|
-
}
|
|
394
|
-
case None => eprintln("[cj-tauri] setMenuItemState: 窗口 \"${label}\" 无应用层菜单模型(只改平台侧)")
|
|
395
|
-
}
|
|
396
|
-
match (this.hostOf(label)) {
|
|
397
|
-
case Some(host) => host.setMenuItemState(id, enabled, checked)
|
|
398
|
-
case None => eprintln("[cj-tauri] setMenuItemState: 未注册的窗口 label \"${label}\"(未生效)")
|
|
399
|
-
}
|
|
400
|
-
}
|
|
401
|
-
|
|
402
|
-
/**
|
|
403
|
-
* 把 shell 事件里的**平台真值**回写到应用层模型(kind=menu、id 命中时)
|
|
404
|
-
*
|
|
405
|
-
* 为什么必须做:勾选项在平台上一被点就自己翻了 checked,模型还记着旧值——之后一次
|
|
406
|
-
* 「只改启用」拿着旧值去写,就把用户刚点出来的勾选抹回去了。
|
|
407
|
-
* 模型 / 项不存在时静默:事件也可能来自没用应用层菜单的窗口(宿主层直设菜单的老路径)。
|
|
408
|
-
*/
|
|
409
|
-
private func syncMenuStateFromShell(label: String, id: String, enabled: Bool, checked: Bool): Unit {
|
|
410
|
-
match (this.menus.get(label)) {
|
|
411
|
-
case Some(model) =>
|
|
412
|
-
match (model.findById(id)) {
|
|
413
|
-
case Some(item) =>
|
|
414
|
-
item.enabled = enabled
|
|
415
|
-
item.checked = checked
|
|
416
|
-
case None => ()
|
|
417
|
-
}
|
|
418
|
-
case None => ()
|
|
419
|
-
}
|
|
420
|
-
}
|
|
421
|
-
|
|
422
|
-
/**
|
|
423
|
-
* 注册命令处理器
|
|
424
|
-
*/
|
|
425
|
-
public func register(cmd: String, handler: CommandHandler): TauriApp {
|
|
426
|
-
this.hub.register(cmd, handler)
|
|
427
|
-
return this
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
/**
|
|
431
|
-
* 装配插件(对标 Tauri v2 的 .plugin(...))
|
|
432
|
-
*
|
|
433
|
-
* 插件的命令会自动加上 "<插件名>:" 前缀后注册;事件登记、前端 JS 与未授权提示
|
|
434
|
-
* 都在 run()/runUrl() 的 prepare() 阶段统一收尾(那时能力清单才加载完)。
|
|
435
|
-
* 必须在 run()/runUrl() 之前调用——页面加载前命令表要定稿。
|
|
436
|
-
*/
|
|
437
|
-
public func plugin(p: Plugin): TauriApp {
|
|
438
|
-
if (!isValidPluginName(p.name())) {
|
|
439
|
-
throw Exception("invalid plugin name: \"${p.name()}\"(要求非空、且不含 ':')")
|
|
440
|
-
}
|
|
441
|
-
this.plugins.add(p)
|
|
442
|
-
return this
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
/**
|
|
446
|
-
* 从 JSON 添加 capability 清单
|
|
447
|
-
*/
|
|
448
|
-
public func addCapabilityJson(json: JsonValue): TauriApp {
|
|
449
|
-
this.capabilities.addFromJson(json)
|
|
450
|
-
return this
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
/**
|
|
454
|
-
* 从目录加载所有 *.json capability 清单(对标 Tauri 的 capabilities/ 目录)
|
|
455
|
-
* 与 run() 的自动扫描等价,只是作用在指定目录上。
|
|
456
|
-
*/
|
|
457
|
-
public func loadCapabilities(dir: String): TauriApp {
|
|
458
|
-
let n = loadCapabilitiesFromDir(dir, this.capabilities)
|
|
459
|
-
eprintln("[cj-tauri] 已从 ${dir} 加载 ${n} 个 capability 清单")
|
|
460
|
-
return this
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
/**
|
|
464
|
-
* 运行期给某插件的 scope 追加一条白名单模式(**不落盘**,重启即失效):插件名如 `"http"`,
|
|
465
|
-
* 模式写法同 `src/scope.cj`(主机名 / 路径,支持 `*` `**` `?` 与变量)。
|
|
466
|
-
*
|
|
467
|
-
* 调用时机是硬约束:**只应在应用已取得用户明确确认之后调**——框架替你做不了确认,它不理解
|
|
468
|
-
* 你的 UI 语义。典型落法是先在原生确认框里问到「是」
|
|
469
|
-
* (`host.showMessageDialog("confirm", …, "允许 <主机> 出网?")` 返回 true)再调本方法;
|
|
470
|
-
* 页面自己点不了原生对话框,所以「确认」这一步天然挡得住被注入的页面自行加白。
|
|
471
|
-
* 另外,清单 `scopes.<插件>.deny` 命中即拒,这里加白掀不掉 deny(判定顺序仍是先 deny 后 allow)。
|
|
472
|
-
*
|
|
473
|
-
* 需要重启仍生效就在启动时把存过的模式重新叠一遍(持久化与存放位置由应用自己决定)。
|
|
474
|
-
*
|
|
475
|
-
* 返回 false = 这条模式**没被采纳**(空串、或含 `*` / `?` 与制表/换行这类脏字符):加白的语义是
|
|
476
|
-
* 「往白名单里加一条具体值」,带通配透传进来等于全放行,所以注册中心当场拒。调用方应把 false
|
|
477
|
-
* 当成「没放行」如实回给用户,别写成成功。
|
|
478
|
-
*/
|
|
479
|
-
public func allowScopePattern(plugin: String, pattern: String): Bool {
|
|
480
|
-
return this.capabilities.addRuntimeScopeAllow(plugin, pattern)
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
/**
|
|
484
|
-
* 某插件的 scope 此刻是否放行某个值(口径与判定完全一致:清单 + 运行期叠加,先 deny 后 allow)。
|
|
485
|
-
* 没写 scope 的插件恒为 true(旧行为:全量放行)。
|
|
486
|
-
*/
|
|
487
|
-
public func isScopeAllowed(plugin: String, value: String): Bool {
|
|
488
|
-
return this.capabilities.scopeAllowsValue(plugin, value)
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
/**
|
|
492
|
-
* 直接添加 capability
|
|
493
|
-
*/
|
|
494
|
-
public func addCapability(cap: Capability): TauriApp {
|
|
495
|
-
this.capabilities.add(cap)
|
|
496
|
-
return this
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
/**
|
|
500
|
-
* 向所有窗口推送事件(对标 Tauri 的 `emit`;经 capability 校验,未声明的事件不投递)
|
|
501
|
-
*/
|
|
502
|
-
public func emit(event: String, payload: JsonValue): Unit {
|
|
503
|
-
this.hub.emitAll(event, payload)
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
/**
|
|
507
|
-
* 向所有窗口推送「只保最新」的事件:同一帧内同名事件只投最后一次 payload,中间值丢弃
|
|
508
|
-
*
|
|
509
|
-
* 给进度 / 动画 / 滚动位置这类高频且「中间值没有意义」的事件用;需要每条都到达的场合继续 `emit`。
|
|
510
|
-
* 能力校验与 `emit` 共用一份 `events` 白名单(不额外放行)。与 Tauri v2 的差异:它核心不做合并,
|
|
511
|
-
* 高频事件要么应用层自己限流、要么改用 `Channel`——我们的选择是给一条显式的 API,
|
|
512
|
-
* 理由与源码级对照见 docs/IPC-通信机制.md §6 第 5 项。
|
|
513
|
-
*/
|
|
514
|
-
public func emitLatest(event: String, payload: JsonValue): Unit {
|
|
515
|
-
this.hub.emitLatest(event, payload, "")
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
/**
|
|
519
|
-
* 向指定窗口推送事件(对标 Tauri 的 `app.emit_to(label, event, payload)`)
|
|
520
|
-
*
|
|
521
|
-
* 多窗口的定向投递入口。label 未注册时**什么都不投**并打一条 stderr——不回落成广播:
|
|
522
|
-
* 写错 label 的表现就该是「谁都没收到」,静默变广播比丢事件更难查(见 WindowRegistry.targets)。
|
|
523
|
-
* 空串 = 广播,与 `emit` 等价。
|
|
524
|
-
*/
|
|
525
|
-
public func emitToWindow(event: String, payload: JsonValue, window: String): Unit {
|
|
526
|
-
this.hub.emitToWindow(event, payload, window)
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
/**
|
|
530
|
-
* 注册后端事件监听器:前端 `emit(event, payload)` 会调用到这里
|
|
531
|
-
* (对标 Tauri Rust 侧的 `app.listen(...)`)
|
|
532
|
-
*
|
|
533
|
-
* 与 `emit` 是相反方向:`emit` 是后端 → 前端,本方法收前端 → 后端。
|
|
534
|
-
* 后端自己 emit 出去的事件**不**回流到监听器(否则「收到就再广播」会自激)。
|
|
535
|
-
* 装配期调用(`run()` / `runUrl()` 之前,或插件的 setup() 里);注册本身不需要能力授权——
|
|
536
|
-
* 权限卡在前端那侧(emit 的事件必须在 capability 的 events 里声明)。
|
|
537
|
-
*/
|
|
538
|
-
public func listenEvent(event: String, handler: (JsonValue) -> Unit): TauriApp {
|
|
539
|
-
this.hub.listenEvent(event, handler)
|
|
540
|
-
return this
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* 读取 CLI 注入的前端 dev server 地址(CJ_TAURI_DEV_URL)
|
|
545
|
-
*
|
|
546
|
-
* 模板可以直接用:
|
|
547
|
-
* if (let Some(url) <- TauriApp.devUrl()) { app.runUrl(url) }
|
|
548
|
-
* else { app.run(html) }
|
|
549
|
-
*/
|
|
550
|
-
public static func devUrl(): Option<String> {
|
|
551
|
-
if (let Some(v) <- getVariable("CJ_TAURI_DEV_URL")) {
|
|
552
|
-
if (v.size > 0) {
|
|
553
|
-
return Some(v)
|
|
554
|
-
}
|
|
555
|
-
}
|
|
556
|
-
return None
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
/**
|
|
560
|
-
* 启动应用:装配 IPC → 启动 WebView 宿主 → 阻塞运行(兼容入口,等价 run(Inline(html)))
|
|
561
|
-
*/
|
|
562
|
-
public func run(html: String): Unit {
|
|
563
|
-
this.run(PageSource.Inline(html))
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
/**
|
|
567
|
-
* 按 URL 启动应用(兼容入口,等价 run(Url(url)))
|
|
568
|
-
*
|
|
569
|
-
* 与 run(html) 平行:同一段装配,只是页面来源换成 URL。有了真实来源,
|
|
570
|
-
* 相对路径资源、动态 import、前端路由才成立(file:// 页面禁止 fetch/XHR)。
|
|
571
|
-
* 开发时可配合 TauriApp.devUrl() 指向前端 dev server(Vite 的 HMR 就这样接进来)。
|
|
572
|
-
*/
|
|
573
|
-
public func runUrl(url: String): Unit {
|
|
574
|
-
this.run(PageSource.Url(url))
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
/**
|
|
578
|
-
* 统一页面入口(批次 1,RFC-002 §4.3):业务与清单只写 PageSource 一种表达
|
|
579
|
-
*
|
|
580
|
-
* - Inline(html):直接给 HTML(探针、错误页)
|
|
581
|
-
* - Asset(name):逻辑名,经注入的 AssetResolver 解析;未注入时按 cwd 相对路径读盘;
|
|
582
|
-
* devMode(CJ_TAURI_DEV_URL 存在)下自动改写为 DevServer(RFC §4.3)
|
|
583
|
-
* - Url(url) / DevServer(url):直载
|
|
584
|
-
*
|
|
585
|
-
* 装配流程与旧入口完全一致:dumpManifest → 插件 shim → 默认窗口 → 附加窗口 → 等待退出。
|
|
586
|
-
*/
|
|
587
|
-
public func run(source: PageSource): Unit {
|
|
588
|
-
let src = rewriteForDev(source, TauriApp.devUrl())
|
|
589
|
-
this.runWith({ h => h.load(src) })
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
/** 装配与启动的共同骨架:所有页面入口在 load(source) 收敛,宿主怎么落地是 L0 的事(RFC §4.3/§6.2) */
|
|
593
|
-
private func runWith(start: (WebViewHost) -> Unit): Unit {
|
|
594
|
-
this.prepare()
|
|
595
|
-
if (this.dumpManifestIfRequested()) {
|
|
596
|
-
return
|
|
597
|
-
}
|
|
598
|
-
this.registerPluginScripts()
|
|
599
|
-
this.registerDefaultWindow()
|
|
600
|
-
// 资产解析器随装配进宿主:桌面读盘、鸿蒙 rawfile 材化,各端 load() 自决怎么用
|
|
601
|
-
this.host.setAssetResolver(this.assetResolver)
|
|
602
|
-
start(this.host)
|
|
603
|
-
this.startExtraWindows()
|
|
604
|
-
this.waitForExit()
|
|
605
|
-
// 事件循环已退出:释放全部宿主编(幂等;句柄由 C 侧回收,见 docs/架构演进-多平台与多窗口.md §6.1)
|
|
606
|
-
this.disposeWindows()
|
|
607
|
-
}
|
|
608
|
-
|
|
609
|
-
/**
|
|
610
|
-
* 清单导出(RFC §8 第 7 条的遗留项:让 `cj-tauri info` 消费 describePluginsJson)
|
|
611
|
-
*
|
|
612
|
-
* 设了 CJ_TAURI_MANIFEST 时,装配一完成就把清单打到 stdout 并返回 true——**不创建窗口**。
|
|
613
|
-
* 于是 CLI / CI 在既没有 X 显示、也没装 WebView2 Runtime 的机器上照样能拿到真实的装配结果。
|
|
614
|
-
* 为什么只能这样拿:CLI 进程里没有应用自己的 TauriApp,插件是应用 main() 里装配的,
|
|
615
|
-
* 所以「不启动窗口,但把 main() 跑到装配完成」是唯一能取到真值的路子(详见 manifestDumpMode)。
|
|
616
|
-
*/
|
|
617
|
-
private func dumpManifestIfRequested(): Bool {
|
|
618
|
-
match (manifestDumpMode()) {
|
|
619
|
-
case Some(mode) =>
|
|
620
|
-
// flush 显式给:CLI 用管道抓 stdout,不能指望进程退出时的缓冲冲刷
|
|
621
|
-
print(renderPluginManifest(this.plugins, mode), flush: true)
|
|
622
|
-
eprintln("[cj-tauri] 已导出插件清单(mode=${mode}):本轮未创建窗口")
|
|
623
|
-
return true
|
|
624
|
-
case None => return false
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* 把插件 jsShim 注册到宿主的「预执行脚本」通道
|
|
630
|
-
*
|
|
631
|
-
* 必须在 load 之前调用:宿主创建 WebView 时一次性交给 C 桥,
|
|
632
|
-
* 桥以 document-start 注入(早于页面脚本,两平台语义一致)。
|
|
633
|
-
* HTML 与 URL 两种来源共用这条通道——v1 那个「runUrl 模式下 shim 注入不进去」的局限就此消掉(RFC §5.4),
|
|
634
|
-
* 所以这里不再需要对使用者的额外提醒。
|
|
635
|
-
*/
|
|
636
|
-
private func registerPluginScripts(): Unit {
|
|
637
|
-
for (js in this.pluginScripts) {
|
|
638
|
-
this.host.addInitScript(js)
|
|
639
|
-
}
|
|
640
|
-
if (this.pluginScripts.size > 0) {
|
|
641
|
-
eprintln("[cj-tauri] 插件 JS 已注册为预执行脚本:${this.pluginScripts.size} 段")
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* 重新加载当前页面(整页 reload,等价于浏览器刷新)
|
|
647
|
-
*/
|
|
648
|
-
public func reload(): Unit {
|
|
649
|
-
this.host.reload()
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
/**
|
|
653
|
-
* 启动前的装配:内置命令、capability、宿主回调与 sink
|
|
654
|
-
*/
|
|
655
|
-
private func prepare(): Unit {
|
|
656
|
-
if (this.started) {
|
|
657
|
-
throw CommandException("app already started")
|
|
658
|
-
}
|
|
659
|
-
this.started = true
|
|
660
|
-
|
|
661
|
-
// 环境变量兜底(RFC-003 第 5 期):应用没显式开日志时,`CJ_TAURI_LOG` 仍能把它开起来——
|
|
662
|
-
// 已经分发出去的 exe 改不了代码,报障时靠这一个变量拿凭证。放在装配的最早处,
|
|
663
|
-
// 让宿主与插件的启动期日志都赶得上;不设这个变量 = 一行不动,行为与之前完全一致。
|
|
664
|
-
if (currentLogFilePath().size == 0) {
|
|
665
|
-
let envPath = logEnvOr("CJ_TAURI_LOG", "")
|
|
666
|
-
if (envPath.size > 0) {
|
|
667
|
-
let _ = this.setLogFile(envPath)
|
|
668
|
-
}
|
|
669
|
-
}
|
|
670
|
-
|
|
671
|
-
// 注入内置系统命令
|
|
672
|
-
this.hub.register("system:version", SystemCommands())
|
|
673
|
-
this.hub.register("system:ping", SystemCommands())
|
|
674
|
-
this.hub.register("system:echo", SystemCommands())
|
|
675
|
-
this.hub.register("system:devtools", SystemCommands())
|
|
676
|
-
this.hub.register("system:host", SystemCommands())
|
|
677
|
-
|
|
678
|
-
// 未显式挂载 capability 时,自动扫描 capabilities/ 目录(对标 Tauri)
|
|
679
|
-
autoLoadCapabilities(this.capabilities)
|
|
680
|
-
|
|
681
|
-
this.hub.setCapabilities(this.capabilities)
|
|
682
|
-
|
|
683
|
-
// 插件装配:注册命令(带 "<插件名>:" 前缀)→ 注册命名权限集 → setup → 汇总前端 JS → 未授权提示。
|
|
684
|
-
// 顺序有意放在能力清单加载之后:提示要反映真实的放行情况。
|
|
685
|
-
for (p in this.plugins) {
|
|
686
|
-
registerPluginCommands(this.hub, p)
|
|
687
|
-
registerPluginPermissionSets(this.capabilities, p)
|
|
688
|
-
p.setup(this)
|
|
689
|
-
}
|
|
690
|
-
this.pluginScripts = pluginInitScripts(this.plugins)
|
|
691
|
-
if (this.plugins.size > 0) {
|
|
692
|
-
eprintln("[cj-tauri] 已装配 ${this.plugins.size} 个插件:")
|
|
693
|
-
eprintln(describePlugins(this.plugins))
|
|
694
|
-
let sets = describePermissionSets(this.plugins)
|
|
695
|
-
if (sets.size > 0) {
|
|
696
|
-
eprintln("[cj-tauri] 命名权限集(在 capabilities 的 permissions 里按集名引用):")
|
|
697
|
-
eprintln(sets)
|
|
698
|
-
}
|
|
699
|
-
warnUnauthorizedPlugins(this.plugins, this.capabilities)
|
|
700
|
-
}
|
|
701
|
-
// 集注册完才可能判定「清单引用的集有没有插件提供」,故放在插件装配之后
|
|
702
|
-
warnUnknownPermissionSets(this.capabilities)
|
|
703
|
-
// 路径 scope 的缺省行为提示(RFC-003 第 4 期):没为吃 scope 的插件写规则 = 全量放行,如实告知
|
|
704
|
-
warnUnscopedPlugins(this.plugins, this.capabilities)
|
|
705
|
-
|
|
706
|
-
// 装配:宿主消息 → IPC hub;hub 输出 → 宿主 JS
|
|
707
|
-
this.wireHostCallbacks(this.host)
|
|
708
|
-
this.hub.jsSink = { json, window =>
|
|
709
|
-
this.deliverJs(json, window)
|
|
710
|
-
}
|
|
711
|
-
this.hub.devToolsSink = { =>
|
|
712
|
-
this.host.openDevTools()
|
|
713
|
-
}
|
|
714
|
-
// system:host 的取值通道:按发起命令的窗口 label 取该宿主的能力位
|
|
715
|
-
this.hub.hostCapabilitiesSink = { label => this.windowCapabilities(label) }
|
|
716
|
-
|
|
717
|
-
this.host.setWindowConfig(this.config)
|
|
718
|
-
// 最小化到托盘同属「建窗时定稿」的窗口行为(窗口过程要提前知道这条策略),
|
|
719
|
-
// 所以与窗口配置一起在 start() 之前交给宿主——能力位不含托盘时平台会拒绝并出声
|
|
720
|
-
this.host.setMinimizeToTray(this.config.minimizeToTray)
|
|
721
|
-
}
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* 某个窗口的宿主能力位(`system:host` 的取值实现)
|
|
725
|
-
*
|
|
726
|
-
* 未登记的 label 给 0 **并出声**:写错 label 的表现会是「能力位全 false」,看起来像
|
|
727
|
-
* 「这个平台不支持菜单」,比一条日志难查得多。
|
|
728
|
-
*/
|
|
729
|
-
private func windowCapabilities(label: String): Int32 {
|
|
730
|
-
match (this.hostOf(label)) {
|
|
731
|
-
case Some(h) => return h.hostCapabilities()
|
|
732
|
-
case None =>
|
|
733
|
-
eprintln("[cj-tauri] system:host: 未注册的窗口 label \"${label}\"(能力位按 0 返回)")
|
|
734
|
-
return 0
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
|
|
738
|
-
/**
|
|
739
|
-
* 把默认窗口登记进注册表(label = "main")并给它注入 label
|
|
740
|
-
*
|
|
741
|
-
* 必须在 load() 之前:① 页面一加载就可能发 invoke / emit,回执要按 label 找到窗口;
|
|
742
|
-
* ② label 走宿主的预执行脚本通道(document-start)注入,晚于 start 就没机会了。
|
|
743
|
-
* 桥 JS 是**懒读** `window.__CJ_TAURI_LABEL__` 的(首次用到才取、取到即缓存),
|
|
744
|
-
* 所以注入脚本与桥 JS 的先后顺序无所谓——两平台的注入时机本就不同。
|
|
745
|
-
* 多窗口落地前只登记这一条;将来增补窗口走同一个 add() + 注入,路由侧不用改。
|
|
746
|
-
*/
|
|
747
|
-
private func registerDefaultWindow(): Unit {
|
|
748
|
-
let label = DEFAULT_WINDOW_LABEL
|
|
749
|
-
this.windows.add(WindowRecord(label, this.host, this.config))
|
|
750
|
-
// JSON 字符串字面量与 JS 字符串字面量同构(双引号、同样的转义),拼进赋值语句是安全的
|
|
751
|
-
this.host.addInitScript("window.__CJ_TAURI_LABEL__ = ${JsonString(label)};")
|
|
752
|
-
// shell 事件分发要在 start 之前装(菜单点击随时可能来,装晚了就漏)
|
|
753
|
-
this.wireShellEvents(this.host, label)
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
/**
|
|
757
|
-
* 把一段回执 / 事件投递给目标窗口(批次 2 的 label 路由)
|
|
758
|
-
*
|
|
759
|
-
* @param window 目标窗口 label;空串 = 广播(按登记顺序投给每个窗口)
|
|
760
|
-
*
|
|
761
|
-
* 未注册的 label 打 stderr 而非静默丢弃:`emitToWindow` 写错 label 的表现是「前端什么都没
|
|
762
|
-
* 收到」,只剩这条日志能定位。空注册表(清单导出模式等)到不了这里——那时没有页面能发消息。
|
|
763
|
-
*/
|
|
764
|
-
private func deliverJs(json: String, window: String): Unit {
|
|
765
|
-
// 直派:JSON 本身就是合法 JS 对象字面量,直接嵌进 eval 交给桥的 _dispatch。
|
|
766
|
-
// 不走 window.postMessage —— 那样每条响应要多一次结构化克隆 + 一次 message 事件派发,
|
|
767
|
-
// 而且那个监听器是全页面(含 iframe / opener)都能投递的伪造入口。
|
|
768
|
-
let js = "window.__CJ_TAURI__._dispatch(${json});"
|
|
769
|
-
let targets = this.windows.targets(window)
|
|
770
|
-
if (targets.size == 0 && window.size > 0) {
|
|
771
|
-
eprintln("[cj-tauri] 未注册的窗口 label:\"${window}\"(回执 / 事件未投递)")
|
|
772
|
-
return
|
|
773
|
-
}
|
|
774
|
-
for (rec in targets) {
|
|
775
|
-
rec.host.runJs(js)
|
|
776
|
-
}
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
/**
|
|
780
|
-
* 阻塞等待:宿主事件循环退出(窗口关闭 / quit)后返回
|
|
781
|
-
*
|
|
782
|
-
* 公开是 AppHost 契约要求的(插件等不必自己写轮询);内部按宿主自己的节奏收尾。
|
|
783
|
-
*/
|
|
784
|
-
public func waitForExit(): Unit {
|
|
785
|
-
var spins: Int64 = 0
|
|
786
|
-
while (!this.allWindowsExited() && spins < 360_000) { // 最长 ~1 小时(10ms 步进)
|
|
787
|
-
sleep(Duration.millisecond * 10)
|
|
788
|
-
spins += 1
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
/**
|
|
793
|
-
* 全部已登记窗口的循环是否都已退出
|
|
794
|
-
*
|
|
795
|
-
* 多窗口的「退出」是**每个窗口都收工**:主窗关了而副窗还开着时,应用不该返回
|
|
796
|
-
* (对标 Tauri:`app.exit()` 收掉全部窗口)。注册表为空(清单导出模式等)时回落到
|
|
797
|
-
* 默认宿主,与单窗口时代的行为逐字等价。
|
|
798
|
-
*/
|
|
799
|
-
private func allWindowsExited(): Bool {
|
|
800
|
-
let recs = this.windows.targets("")
|
|
801
|
-
if (recs.size == 0) {
|
|
802
|
-
return this.host.shouldQuit()
|
|
803
|
-
}
|
|
804
|
-
for (rec in recs) {
|
|
805
|
-
if (!rec.host.shouldQuit()) {
|
|
806
|
-
return false
|
|
807
|
-
}
|
|
808
|
-
}
|
|
809
|
-
return true
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
/**
|
|
813
|
-
* 释放全部宿主编(幂等,见 WebViewHost.dispose 的契约)
|
|
814
|
-
*/
|
|
815
|
-
private func disposeWindows(): Unit {
|
|
816
|
-
for (rec in this.windows.targets("")) {
|
|
817
|
-
rec.host.dispose()
|
|
818
|
-
}
|
|
819
|
-
}
|
|
820
|
-
|
|
821
|
-
/**
|
|
822
|
-
* 把一个宿主挂上应用级回调
|
|
823
|
-
*
|
|
824
|
-
* 多窗口下每个宿主都挂**同一只**派发器:报文自带发起窗口的 label(架构文档 §7.3),
|
|
825
|
-
* 路由不需要按句柄分表——所以这里传的是同一个闭包,不是每窗口一份。
|
|
826
|
-
*/
|
|
827
|
-
/**
|
|
828
|
-
* 应用层的 shell 事件钩子(对标 Tauri 的 `on_menu_event`)
|
|
829
|
-
*
|
|
830
|
-
* 回调参数是 `(信封 JSON, 发生事件的窗口 label)`。给原始 JSON 而不是解析后的对象:钩子多半
|
|
831
|
-
* 是应用自己写的诊断 / 日志(探针就靠它断言),原样透传最不容易丢信息。label 是必需的——
|
|
832
|
-
* 钩子是应用级的,一份回调要能分辨事件来自哪个窗口。
|
|
833
|
-
*
|
|
834
|
-
* 与前端事件**两条路都走**:先按注册顺序跑钩子(应用侧决策,例如改菜单项状态),再投前端
|
|
835
|
-
* 事件(页面自己 `listen("menu:click", …)`)。钩子里抛异常只留一行 stderr,不挡后面的钩子
|
|
836
|
-
* 与前端事件。
|
|
837
|
-
*/
|
|
838
|
-
public func onShellEvent(handler: (String, String) -> Unit): TauriApp {
|
|
839
|
-
this.shellHooks.add(handler)
|
|
840
|
-
return this
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
/**
|
|
844
|
-
* 装配本窗口的 shell 事件分发(应用层是唯一安装者)
|
|
845
|
-
*
|
|
846
|
-
* 必须在宿主 `start()` / `startUrl()` **之前**装:窗口一建出来菜单点击就可能来,装晚了会漏。
|
|
847
|
-
* 宿主层的 `setShellHandler` 是**可变槽位**(后装覆盖先装),所以应用要处理 shell 事件请用
|
|
848
|
-
* `onShellEvent()`(应用级钩子,可叠加多条),不要和框架抢宿主槽位——抢赢的那个会让框架的
|
|
849
|
-
* 前端事件分发失效。
|
|
850
|
-
*/
|
|
851
|
-
private func wireShellEvents(host: WebViewHost, label: String): Unit {
|
|
852
|
-
host.setShellHandler({ json => this.dispatchShellEvent(label, json) })
|
|
853
|
-
}
|
|
854
|
-
|
|
855
|
-
/**
|
|
856
|
-
* shell 事件 → 应用层钩子 + 前端事件(按窗口投递)
|
|
857
|
-
*
|
|
858
|
-
* 路由靠**闭包捕获的 label**:C 侧已按宿主句柄把事件分给了本窗口的回调(见 host.cj 的
|
|
859
|
-
* `ShellRoute`),这里再按 label 投前端事件,多窗口下不会串。事件放不放行由 capability 清单
|
|
860
|
-
* 决定——没在清单里声明 `menu:click` 就是静默丢弃(最小权限不破)。
|
|
861
|
-
*
|
|
862
|
-
* 本函数跑在**宿主 UI 线程**上(C 桥的 shell 回调就在那儿),所以中途不碰 GTK / WebKit 之外
|
|
863
|
-
* 的东西:投递走 `emitToWindow` → `jsSink`,与其它事件同一条路。
|
|
864
|
-
*/
|
|
865
|
-
private func dispatchShellEvent(label: String, json: String): Unit {
|
|
866
|
-
for (hook in this.shellHooks) {
|
|
867
|
-
try {
|
|
868
|
-
hook(json, label)
|
|
869
|
-
} catch (e: Exception) {
|
|
870
|
-
eprintln("[cj-tauri] shell hook 抛出异常(已忽略):${e}")
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
match (parseShellEnvelope(json)) {
|
|
874
|
-
case None => eprintln("[cj-tauri] shell event 信封解析失败(已丢弃):${json}")
|
|
875
|
-
case Some(obj) =>
|
|
876
|
-
var kind = ""
|
|
877
|
-
if (let Some(k) <- shellString(obj, "kind")) {
|
|
878
|
-
kind = k
|
|
879
|
-
}
|
|
880
|
-
match (shellEventNameFor(kind)) {
|
|
881
|
-
case None => eprintln("[cj-tauri] shell event: 未知 kind \"${kind}\"(未投递):${json}")
|
|
882
|
-
case Some(event) =>
|
|
883
|
-
if (kind == "menu") {
|
|
884
|
-
this.syncMenuStateFromShellObj(label, obj)
|
|
885
|
-
}
|
|
886
|
-
try {
|
|
887
|
-
this.hub.emitToWindow(event, obj, label)
|
|
888
|
-
} catch (e: Exception) {
|
|
889
|
-
eprintln("[cj-tauri] shell event 投递失败(已忽略):${e}")
|
|
890
|
-
}
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
|
|
895
|
-
/**
|
|
896
|
-
* 菜单事件里的平台真值 → 应用层模型:id / enabled / checked 三者齐了才写
|
|
897
|
-
*
|
|
898
|
-
* 缺字段不拿默认值顶(见 shellBool 的注释)——这条路径写错的表现是「勾选莫名回退」,
|
|
899
|
-
* 比少同步一次难查得多。
|
|
900
|
-
*/
|
|
901
|
-
private func syncMenuStateFromShellObj(label: String, obj: JsonObject): Unit {
|
|
902
|
-
if (let Some(id) <- shellString(obj, "id")) {
|
|
903
|
-
if (let Some(enabled) <- shellBool(obj, "enabled")) {
|
|
904
|
-
if (let Some(checked) <- shellBool(obj, "checked")) {
|
|
905
|
-
this.syncMenuStateFromShell(label, id, enabled, checked)
|
|
906
|
-
}
|
|
907
|
-
}
|
|
908
|
-
}
|
|
909
|
-
}
|
|
910
|
-
|
|
911
|
-
private func wireHostCallbacks(host: WebViewHost): Unit {
|
|
912
|
-
host.setMessageHandler({ raw =>
|
|
913
|
-
this.hub.handleRawMessage(raw)
|
|
914
|
-
})
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* 启动 addWindow() 登记过的附加窗口
|
|
919
|
-
*
|
|
920
|
-
* 放在默认窗口 start() 之后:主窗先出现,附加窗口紧随其后(顺序只影响观感与探针取证节奏)。
|
|
921
|
-
* 每个附加窗口在启动前挂上应用级回调——与默认窗口同一只派发器。
|
|
922
|
-
*/
|
|
923
|
-
private func startExtraWindows(): Unit {
|
|
924
|
-
for (w in this.extraWindows) {
|
|
925
|
-
this.wireHostCallbacks(w.host)
|
|
926
|
-
// shell 事件分发要在 load 之前装(菜单点击随时可能来,装晚了就漏)
|
|
927
|
-
this.wireShellEvents(w.host, w.label)
|
|
928
|
-
w.host.load(PageSource.Inline(w.html))
|
|
929
|
-
eprintln("[cj-tauri] 附加窗口已启动:label=\"${w.label}\"")
|
|
930
|
-
}
|
|
931
|
-
}
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
/**
|
|
935
|
-
* 清单导出模式(环境变量 CJ_TAURI_MANIFEST)
|
|
936
|
-
*
|
|
937
|
-
* 取值:`json` → 机器可读 JSON(`describePluginsJson`);其它非空值 → 人类可读表(`describePlugins`);
|
|
938
|
-
* 未设置(或空串)→ None,即正常启动窗口。
|
|
939
|
-
*
|
|
940
|
-
* 为什么走环境变量,而不是给 TauriApp 加个方法:清单要**装配完成之后**才有值,而装配发生在
|
|
941
|
-
* run() / runUrl() 内部——加方法就得让每个应用的 main() 多写一个分支,既有应用全都要改。
|
|
942
|
-
* 环境变量让既有应用一行不动就能被 CLI 取出清单(就是 `cj-tauri info` 拿插件清单的通道)。
|
|
943
|
-
*/
|
|
944
|
-
func manifestDumpMode(): Option<String> {
|
|
945
|
-
if (let Some(v) <- getVariable("CJ_TAURI_MANIFEST")) {
|
|
946
|
-
if (v.size == 0) {
|
|
947
|
-
return None
|
|
948
|
-
}
|
|
949
|
-
if (v == "json") {
|
|
950
|
-
return Some("json")
|
|
951
|
-
}
|
|
952
|
-
return Some("text")
|
|
953
|
-
}
|
|
954
|
-
return None
|
|
955
|
-
}
|
|
956
|
-
|
|
957
|
-
/**
|
|
958
|
-
* 渲染清单文本:`json` 模式给机器(工具 / CI 直接解析),其余给人(启动日志里那份表的同款形态)。
|
|
959
|
-
* 抽成纯函数是为了单测能直接断言,不必真去跑一次宿主。
|
|
960
|
-
*/
|
|
961
|
-
func renderPluginManifest(plugins: ArrayList<Plugin>, mode: String): String {
|
|
962
|
-
if (mode == "json") {
|
|
963
|
-
return describePluginsJson(plugins).toJsonString()
|
|
964
|
-
}
|
|
965
|
-
if (plugins.size == 0) {
|
|
966
|
-
return "(未装配插件)\n"
|
|
967
|
-
}
|
|
968
|
-
return describePlugins(plugins)
|
|
969
|
-
}
|
|
1
|
+
package cjTauri
|
|
2
|
+
|
|
3
|
+
import std.collection.*
|
|
4
|
+
import std.env.*
|
|
5
|
+
import stdx.encoding.json.*
|
|
6
|
+
import cjTauri.platform.*
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 附加窗口的登记项(试验性 API `TauriApp.addWindow` 的装配状态)
|
|
10
|
+
*
|
|
11
|
+
* 为什么宿主在这里就建好、html 也先留着:窗口表要在 run() 之前定稿(`hostOf` 当场可用),
|
|
12
|
+
* 而原生窗口要等 run() 才创建——两者分开存,正好对上宿主自己的两段式节奏
|
|
13
|
+
* (宿主对象构造时就有、原生窗口 start() 时才建)。
|
|
14
|
+
*/
|
|
15
|
+
class ExtraWindowSpec {
|
|
16
|
+
let label: String
|
|
17
|
+
let host: WebViewHost
|
|
18
|
+
let html: String
|
|
19
|
+
|
|
20
|
+
init(label: String, host: WebViewHost, html: String) {
|
|
21
|
+
this.label = label
|
|
22
|
+
this.host = host
|
|
23
|
+
this.html = html
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* TauriApp 装配(对标 tauri::Builder / App)
|
|
29
|
+
*
|
|
30
|
+
* 用法:
|
|
31
|
+
* let app = TauriApp()
|
|
32
|
+
* app.register("greet", GreetCommand())
|
|
33
|
+
* app.addCapabilityJson(...) // 或 addCapability(cap)
|
|
34
|
+
* app.run(html) // 启动宿主并阻塞
|
|
35
|
+
*
|
|
36
|
+
* 多窗口(试验性,批次 3 的最小切片):`addWindow(label, config, html)` 登记附加窗口,
|
|
37
|
+
* run() / runUrl() 统一启动;事件按 label 路由(`emitToWindow` / `hostOf`)。
|
|
38
|
+
*
|
|
39
|
+
* **本文件只放「应用是什么」的声明与装配 API**(字段、构造、注册命令 / 插件 / 能力、窗口与宿主查询)。
|
|
40
|
+
* 其余同职责的分片(都是 `extend TauriApp`,包边界与对外 API 完全不变):
|
|
41
|
+
* - `src/app_run.cj` —— 启动 / 退出 / 清单导出(run 三入口、prepare、事件循环等待)
|
|
42
|
+
* - `src/app_events.cj` —— 事件收发与 shell 事件分发(emit 三兄弟、listenEvent、deliverJs)
|
|
43
|
+
* - `src/app_host_ops.cj` —— 宿主能力预置(菜单 / 托盘 / 快捷键 / 拖放 / 可见性 / 日志)
|
|
44
|
+
*
|
|
45
|
+
* 拆分的代价(有意接受):仓颉的 `extend` 碰不到 `private` 成员,所以**被跨文件使用的字段与
|
|
46
|
+
* 少数内部方法在这里是包内可见**(不写修饰符)而不是 `private`。对包外完全不可见——
|
|
47
|
+
* 子包(`cjTauri.plugins.*` / `cjTauri.tests`)只认 `public`,应用侧看到的 API 一行未变。
|
|
48
|
+
*/
|
|
49
|
+
public class TauriApp <: AppHost {
|
|
50
|
+
// 以下字段不写 private:同包分片(app_run / app_events / app_host_ops)经 extend 访问它们,
|
|
51
|
+
// 而 extend 看不到 private 成员。包可见已是最小代价,见类注释。
|
|
52
|
+
var hub: IpcHub = IpcHub()
|
|
53
|
+
var capabilities: CapabilityRegistry = CapabilityRegistry()
|
|
54
|
+
// 批次 1(RFC-002 §5.4):注册表优先、内置实现兜底
|
|
55
|
+
var host: WebViewHost = resolveHost(DEFAULT_WINDOW_LABEL)
|
|
56
|
+
// Asset 页面的解析器(批次 1 可注入;缺省按 cwd 相对路径读盘,清单派生在批次 2)
|
|
57
|
+
var assetResolver: Option<AssetResolver> = None
|
|
58
|
+
var config: WindowConfig = WindowConfig()
|
|
59
|
+
// 窗口注册表(批次 2):单窗口阶段只有一条 "main",多窗口时是「label → 宿主」的唯一权威来源
|
|
60
|
+
var windows: WindowRegistry = WindowRegistry()
|
|
61
|
+
// 附加窗口(批次 3 多窗口 UI 的最小切片,试验性):addWindow() 登记、run() / runUrl() 统一启动
|
|
62
|
+
var extraWindows: ArrayList<ExtraWindowSpec> = ArrayList<ExtraWindowSpec>()
|
|
63
|
+
// 应用层留存的菜单模型(label → 模型):setMenu() 记一份,供「只改某一维状态」反查另一维
|
|
64
|
+
var menus: HashMap<String, MenuModel> = HashMap<String, MenuModel>()
|
|
65
|
+
// 应用层的 shell 事件钩子(对标 Tauri 的 on_menu_event):框架分发时先跑钩子、再投前端事件
|
|
66
|
+
var shellHooks: ArrayList<(String, String) -> Unit> = ArrayList<(String, String) -> Unit>()
|
|
67
|
+
var started: Bool = false
|
|
68
|
+
var plugins: ArrayList<Plugin> = ArrayList<Plugin>()
|
|
69
|
+
// 各插件 jsShim() 收集成的「预执行脚本」列表,prepare() 时定稿,run / runUrl 时交给宿主
|
|
70
|
+
var pluginScripts: ArrayList<String> = ArrayList<String>()
|
|
71
|
+
|
|
72
|
+
public init() {}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 设置窗口配置(标题 / 尺寸),对标 tauri.conf.json 的 app.windows
|
|
76
|
+
*/
|
|
77
|
+
public func window(config: WindowConfig): TauriApp {
|
|
78
|
+
this.config = config
|
|
79
|
+
return this
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 注入资产解析器(批次 1,RFC-002 §6)
|
|
84
|
+
*
|
|
85
|
+
* 供 run(Asset("ui/index.html")) 取页面与资产;不注入时按 cwd 相对路径读盘,
|
|
86
|
+
* 与现有模板 textOrFile 的桌面行为等价。鸿蒙 rawfile / 内嵌常量的实现由
|
|
87
|
+
* 各端适配层提供,批次 2 随清单派生接入。
|
|
88
|
+
*/
|
|
89
|
+
public func assets(resolver: AssetResolver): TauriApp {
|
|
90
|
+
this.assetResolver = Some(resolver)
|
|
91
|
+
return this
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 按逻辑名读一份资产文本(如 `ui/vendor/marked.min.js`),走装配时登记的解析器:
|
|
96
|
+
* 桌面 = DirectoryAssetResolver(assets/ 下按名读盘),鸿蒙 = RawfileAssetResolver(hap rawfile)。
|
|
97
|
+
* 给业务侧的窄口——vendor 库注入这类「装配期要拿资产原文」的需求不必自己碰解析器字段。
|
|
98
|
+
* 未登记解析器或读不到都回 None,调用方按「没有 = 降级」处理(如 stderr 提示 + 退化渲染)。
|
|
99
|
+
*/
|
|
100
|
+
public func readAsset(name: String): Option<String> {
|
|
101
|
+
match (this.assetResolver) {
|
|
102
|
+
case Some(r) => return r.readText(name)
|
|
103
|
+
case None => return None
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* 登记一个附加窗口(**试验性**:批次 3「多窗口 UI」的最小切片,先给出可验证的通路)
|
|
109
|
+
*
|
|
110
|
+
* 语义:本方法只**登记 + 装配宿主**,原生窗口在 run() / runUrl() 里统一创建——
|
|
111
|
+
* 与默认窗口「构造时有宿主对象、start() 时才建窗口」是同一套节奏(WebViewHost 的契约)。
|
|
112
|
+
* 因此必须在 run() / runUrl() 之前调用;登记后 `hostOf(label)` 立刻可用。
|
|
113
|
+
*
|
|
114
|
+
* 现在**只接内联 HTML**(URL 变体等真实需求出现再加,判据见架构文档 §5);
|
|
115
|
+
* 空 label / 重复 label 在调用点直接抛异常(WindowRegistry.add 的约定,不延迟到 run)。
|
|
116
|
+
*
|
|
117
|
+
* 已知限制(**未解决**,架构文档 §7.5 的残余项):`onDestroy` 与对话框结果回调不带宿主 id,
|
|
118
|
+
* 多窗口下分不清是哪个窗口关的;对话框仍是同一时刻一个(桥侧互斥)。本方法不解这两条。
|
|
119
|
+
*/
|
|
120
|
+
public func addWindow(label: String, config: WindowConfig, html: String): TauriApp {
|
|
121
|
+
if (this.started) {
|
|
122
|
+
throw CommandException("addWindow 必须在 run() / runUrl() 之前调用(页面加载前窗口表要定稿)")
|
|
123
|
+
}
|
|
124
|
+
let host = resolveHost(label)
|
|
125
|
+
host.setWindowConfig(config)
|
|
126
|
+
// label 注入与默认窗口走同一条通道(document-start 预执行脚本,桥 JS 懒读,先后顺序无所谓)
|
|
127
|
+
host.addInitScript("window.__CJ_TAURI_LABEL__ = ${JsonString(label)};")
|
|
128
|
+
// 登记放在这里(而不是 run 里):重复 label / 空 label 当场报错,调用点即可见
|
|
129
|
+
this.windows.add(WindowRecord(label, host, config))
|
|
130
|
+
this.extraWindows.add(ExtraWindowSpec(label, host, html))
|
|
131
|
+
return this
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 运行时打开开发者工具(等价于前端 invoke("system:devtools"))
|
|
136
|
+
*/
|
|
137
|
+
public func openDevTools(): Unit {
|
|
138
|
+
this.host.openDevTools()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 重新加载当前页面(整页 reload,等价于浏览器刷新)
|
|
143
|
+
*/
|
|
144
|
+
public func reload(): Unit {
|
|
145
|
+
this.host.reload()
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* 宿主实例(插件的 setup() 取用)
|
|
150
|
+
*
|
|
151
|
+
* 需要窗口 / 原生能力的插件命令(原生对话框等)走它;纯仓颉插件(如 fs)用不到。
|
|
152
|
+
* 宿主对象在构造 TauriApp 时就已存在,但**原生窗口要等 run()/runUrl() 才创建**——
|
|
153
|
+
* 依赖窗口的操作必须在启动之后调用,实现方自己会给出可读的降级结果。
|
|
154
|
+
*/
|
|
155
|
+
public func webviewHost(): WebViewHost {
|
|
156
|
+
return this.host
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* 按 label 取窗口宿主(AppHost 契约,对标 `app.get_webview_window(label)`);未注册返回 None
|
|
161
|
+
*
|
|
162
|
+
* 单窗口阶段只可能命中 "main";多窗口落地后这是插件拿到「别的窗口」的唯一入口。
|
|
163
|
+
*/
|
|
164
|
+
public func hostOf(label: String): Option<WebViewHost> {
|
|
165
|
+
match (this.windows.get(label)) {
|
|
166
|
+
case Some(rec) => return Some(rec.host)
|
|
167
|
+
case None =>
|
|
168
|
+
// 默认窗口的注册表登记发生在 prepare()(那时窗口配置才定稿),所以 run() 之前
|
|
169
|
+
// 查 "main" 会落空——但宿主对象在 TauriApp 构造时就已经存在。
|
|
170
|
+
// 这里按 label 兜住,让「run() 之前 hostOf(label) 可用」对默认窗与 addWindow
|
|
171
|
+
// 登记的窗一致:预置菜单 / 拖放策略都必须在 start() 之前调(RFC-菜单-托盘-拖放 §5.2),
|
|
172
|
+
// 默认窗拿不到宿主就等于这条通路对最常见的用法直接不存在。
|
|
173
|
+
if (label == DEFAULT_WINDOW_LABEL) {
|
|
174
|
+
return Some(this.host)
|
|
175
|
+
}
|
|
176
|
+
return None
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* 退出应用(AppHost 契约,对标 Tauri 的 `app.exit()`):收掉所有窗口的事件循环,run() / runUrl() 随即返回
|
|
182
|
+
*
|
|
183
|
+
* 单窗口下与宿主自己的 `quit()` 等价;多窗口时这里是「先收各窗口的循环、再各自收尾」的唯一入口——
|
|
184
|
+
* 所以命令处理器里退应用走它,不要直接调 `webviewHost().quit()`(那只是本窗口的低层原语)。
|
|
185
|
+
*/
|
|
186
|
+
public func quit(): Unit {
|
|
187
|
+
for (rec in this.windows.targets("")) {
|
|
188
|
+
rec.host.quit()
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* 阻塞等待:宿主事件循环退出(窗口关闭 / quit)后返回
|
|
194
|
+
*
|
|
195
|
+
* 公开是 AppHost 契约要求的(插件等不必自己写轮询);内部按宿主自己的节奏收尾。
|
|
196
|
+
*
|
|
197
|
+
* 为什么它在这里而不在 `app_run.cj`:接口成员只能由类体实现,`extend` 里补写不算实现
|
|
198
|
+
* (编译报 `unimplemented interface function 'waitForExit'`)。判定与退出判定
|
|
199
|
+
* `allWindowsExited()` 因此分居两个文件——那一个已经放宽成包内可见。
|
|
200
|
+
*/
|
|
201
|
+
public func waitForExit(): Unit {
|
|
202
|
+
var spins: Int64 = 0
|
|
203
|
+
while (!this.allWindowsExited() && spins < 360_000) { // 最长 ~1 小时(10ms 步进)
|
|
204
|
+
sleep(Duration.millisecond * 10)
|
|
205
|
+
spins += 1
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* 全部已登记窗口的循环是否都已退出
|
|
211
|
+
*
|
|
212
|
+
* 多窗口的「退出」是**每个窗口都收工**:主窗关了而副窗还开着时,应用不该返回
|
|
213
|
+
* (对标 Tauri:`app.exit()` 收掉全部窗口)。注册表为空(清单导出模式等)时回落到
|
|
214
|
+
* 默认宿主,与单窗口时代的行为逐字等价。
|
|
215
|
+
*
|
|
216
|
+
* 为什么它和 `waitForExit` 一起留在类体:类体的方法**看不到** `extend` 里新增的成员
|
|
217
|
+
* (编译报 `'allWindowsExited' is not a member of class 'TauriApp'`),所以「类体 → extend」
|
|
218
|
+
* 这条方向不能有调用。判定与被判定只能同侧。
|
|
219
|
+
*/
|
|
220
|
+
private func allWindowsExited(): Bool {
|
|
221
|
+
let recs = this.windows.targets("")
|
|
222
|
+
if (recs.size == 0) {
|
|
223
|
+
return this.host.shouldQuit()
|
|
224
|
+
}
|
|
225
|
+
for (rec in recs) {
|
|
226
|
+
if (!rec.host.shouldQuit()) {
|
|
227
|
+
return false
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return true
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* 注册命令处理器
|
|
235
|
+
*/
|
|
236
|
+
public func register(cmd: String, handler: CommandHandler): TauriApp {
|
|
237
|
+
this.hub.register(cmd, handler)
|
|
238
|
+
return this
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* 装配插件(对标 Tauri v2 的 .plugin(...))
|
|
243
|
+
*
|
|
244
|
+
* 插件的命令会自动加上 "<插件名>:" 前缀后注册;事件登记、前端 JS 与未授权提示
|
|
245
|
+
* 都在 run()/runUrl() 的 prepare() 阶段统一收尾(那时能力清单才加载完)。
|
|
246
|
+
* 必须在 run()/runUrl() 之前调用——页面加载前命令表要定稿。
|
|
247
|
+
*/
|
|
248
|
+
public func plugin(p: Plugin): TauriApp {
|
|
249
|
+
if (!isValidPluginName(p.name())) {
|
|
250
|
+
throw Exception("invalid plugin name: \"${p.name()}\"(要求非空、且不含 ':')")
|
|
251
|
+
}
|
|
252
|
+
this.plugins.add(p)
|
|
253
|
+
return this
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 从 JSON 添加 capability 清单
|
|
258
|
+
*/
|
|
259
|
+
public func addCapabilityJson(json: JsonValue): TauriApp {
|
|
260
|
+
this.capabilities.addFromJson(json)
|
|
261
|
+
return this
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* 从目录加载所有 *.json capability 清单(对标 Tauri 的 capabilities/ 目录)
|
|
266
|
+
* 与 run() 的自动扫描等价,只是作用在指定目录上。
|
|
267
|
+
*/
|
|
268
|
+
public func loadCapabilities(dir: String): TauriApp {
|
|
269
|
+
let n = loadCapabilitiesFromDir(dir, this.capabilities)
|
|
270
|
+
eprintln("[cj-tauri] 已从 ${dir} 加载 ${n} 个 capability 清单")
|
|
271
|
+
return this
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* 运行期给某插件的 scope 追加一条白名单模式(**不落盘**,重启即失效):插件名如 `"http"`,
|
|
276
|
+
* 模式写法同 `src/scope.cj`(主机名 / 路径,支持 `*` `**` `?` 与变量)。
|
|
277
|
+
*
|
|
278
|
+
* 调用时机是硬约束:**只应在应用已取得用户明确确认之后调**——框架替你做不了确认,它不理解
|
|
279
|
+
* 你的 UI 语义。典型落法是先在原生确认框里问到「是」
|
|
280
|
+
* (`host.showMessageDialog("confirm", …, "允许 <主机> 出网?")` 返回 true)再调本方法;
|
|
281
|
+
* 页面自己点不了原生对话框,所以「确认」这一步天然挡得住被注入的页面自行加白。
|
|
282
|
+
* 另外,清单 `scopes.<插件>.deny` 命中即拒,这里加白掀不掉 deny(判定顺序仍是先 deny 后 allow)。
|
|
283
|
+
*
|
|
284
|
+
* 需要重启仍生效就在启动时把存过的模式重新叠一遍(持久化与存放位置由应用自己决定)。
|
|
285
|
+
*
|
|
286
|
+
* 返回 false = 这条模式**没被采纳**(空串、或含 `*` / `?` 与制表/换行这类脏字符):加白的语义是
|
|
287
|
+
* 「往白名单里加一条具体值」,带通配透传进来等于全放行,所以注册中心当场拒。调用方应把 false
|
|
288
|
+
* 当成「没放行」如实回给用户,别写成成功。
|
|
289
|
+
*/
|
|
290
|
+
public func allowScopePattern(plugin: String, pattern: String): Bool {
|
|
291
|
+
return this.capabilities.addRuntimeScopeAllow(plugin, pattern)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* 某插件的 scope 此刻是否放行某个值(口径与判定完全一致:清单 + 运行期叠加,先 deny 后 allow)。
|
|
296
|
+
* 没写 scope 的插件恒为 true(旧行为:全量放行)。
|
|
297
|
+
*/
|
|
298
|
+
public func isScopeAllowed(plugin: String, value: String): Bool {
|
|
299
|
+
return this.capabilities.scopeAllowsValue(plugin, value)
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* 直接添加 capability
|
|
304
|
+
*/
|
|
305
|
+
public func addCapability(cap: Capability): TauriApp {
|
|
306
|
+
this.capabilities.add(cap)
|
|
307
|
+
return this
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* 读取 CLI 注入的前端 dev server 地址(CJ_TAURI_DEV_URL)
|
|
312
|
+
*
|
|
313
|
+
* 模板可以直接用:
|
|
314
|
+
* if (let Some(url) <- TauriApp.devUrl()) { app.runUrl(url) }
|
|
315
|
+
* else { app.run(html) }
|
|
316
|
+
*/
|
|
317
|
+
public static func devUrl(): Option<String> {
|
|
318
|
+
if (let Some(v) <- getVariable("CJ_TAURI_DEV_URL")) {
|
|
319
|
+
if (v.size > 0) {
|
|
320
|
+
return Some(v)
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
return None
|
|
324
|
+
}
|
|
325
|
+
}
|