cj-tauri 2.0.0 → 2.1.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.
Files changed (157) hide show
  1. package/README.md +206 -139
  2. package/bin/cj-tauri.js +287 -287
  3. package/cjpm.toml +63 -52
  4. package/cli/cjpm.toml +22 -22
  5. package/cli/src/doctor.cj +308 -306
  6. package/cli/src/main.cj +129 -129
  7. package/cli/src/manifest.cj +643 -558
  8. package/cli/src/manifest_test.cj +87 -0
  9. package/cli/src/project.cj +770 -770
  10. package/cli/src/resolve.cj +348 -348
  11. package/cli/src/scaffold.cj +369 -369
  12. package/cli/src/sync.cj +638 -526
  13. package/cli/src/util.cj +168 -168
  14. package/cli/templates/app/README.md +51 -48
  15. package/cli/templates/app/assets/capabilities/default.json +7 -7
  16. package/cli/templates/app/assets/ui/index.html +120 -118
  17. package/cli/templates/app/cj-tauri.toml +32 -22
  18. package/cli/templates/app/cjpm.toml +34 -34
  19. package/cli/templates/app/src/main.cj +101 -90
  20. package/cli/templates/app-react/README.md +56 -56
  21. package/cli/templates/app-react/assets/capabilities/default.json +7 -7
  22. package/cli/templates/app-react/assets/ui/index.html +25 -25
  23. package/cli/templates/app-react/assets/ui/package.json +19 -19
  24. package/cli/templates/app-react/assets/ui/src/App.css +67 -67
  25. package/cli/templates/app-react/assets/ui/src/App.jsx +124 -124
  26. package/cli/templates/app-react/assets/ui/src/main.jsx +6 -6
  27. package/cli/templates/app-react/assets/ui/vite.config.js +24 -24
  28. package/cli/templates/app-react/cj-tauri.toml +33 -23
  29. package/cli/templates/app-react/cjpm.toml +34 -34
  30. package/cli/templates/app-react/src/main.cj +91 -91
  31. package/cli/templates/app-vue/README.md +56 -56
  32. package/cli/templates/app-vue/assets/capabilities/default.json +7 -7
  33. package/cli/templates/app-vue/assets/ui/index.html +25 -25
  34. package/cli/templates/app-vue/assets/ui/package.json +18 -18
  35. package/cli/templates/app-vue/assets/ui/src/App.vue +176 -176
  36. package/cli/templates/app-vue/assets/ui/src/main.js +4 -4
  37. package/cli/templates/app-vue/assets/ui/vite.config.js +24 -24
  38. package/cli/templates/app-vue/cj-tauri.toml +33 -23
  39. package/cli/templates/app-vue/cjpm.toml +34 -34
  40. package/cli/templates/app-vue/src/main.cj +91 -91
  41. package/cli/templates/ohos-skeleton/AppScope/app.json5 +13 -13
  42. package/cli/templates/ohos-skeleton/AppScope/resources/base/element/string.json +8 -8
  43. package/cli/templates/ohos-skeleton/AppScope/resources/base/media/layered_image.json +6 -6
  44. package/cli/templates/ohos-skeleton/README.md +84 -79
  45. package/cli/templates/ohos-skeleton/build-hap.sh +15 -0
  46. package/cli/templates/ohos-skeleton/build-profile.json5 +46 -46
  47. package/cli/templates/ohos-skeleton/entry/build-profile.json5 +26 -26
  48. package/cli/templates/ohos-skeleton/entry/cjpm.toml +51 -43
  49. package/cli/templates/ohos-skeleton/entry/hvigorfile.ts +6 -6
  50. package/cli/templates/ohos-skeleton/entry/oh-package.json5 +9 -9
  51. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/ability_stage.cj +17 -17
  52. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/app_host.cj +94 -94
  53. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
  54. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -167
  55. package/cli/templates/ohos-skeleton/entry/src/main/cangjie/main_ability.cj +139 -139
  56. package/cli/templates/ohos-skeleton/entry/src/main/module.json5 +57 -49
  57. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/color.json +8 -8
  58. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/element/string.json +16 -16
  59. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/media/layered_image.json +6 -6
  60. package/cli/templates/ohos-skeleton/entry/src/main/resources/base/profile/main_pages.json +2 -2
  61. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/capabilities.json +6 -6
  62. package/cli/templates/ohos-skeleton/entry/src/main/resources/rawfile/index.html +92 -92
  63. package/cli/templates/ohos-skeleton/hvigor/hvigor-config.json5 +5 -5
  64. package/cli/templates/ohos-skeleton/hvigorfile.ts +6 -6
  65. package/cli/templates/ohos-skeleton/oh-package.json5 +10 -10
  66. package/native/build_win.bat +62 -62
  67. package/native/core/bridge_core.c +1681 -1626
  68. package/native/core/bridge_core.h +675 -618
  69. package/native/core/bridge_js.h +108 -108
  70. package/native/platforms/harmonyos/README.md +115 -6
  71. package/native/platforms/linux/bridge_linux.c +2787 -2485
  72. package/native/platforms/windows/bridge_win.c +3745 -3248
  73. package/native/tests/gtk_threading_probe.c +143 -143
  74. package/native/tests/test_bridge_core.c +1339 -1228
  75. package/package.json +53 -53
  76. package/src/api_system.cj +67 -67
  77. package/src/app.cj +335 -969
  78. package/src/app_events.cj +166 -0
  79. package/src/app_host_ops.cj +287 -0
  80. package/src/app_run.cj +283 -0
  81. package/src/app_spec.cj +76 -76
  82. package/src/arkweb_menu.cj +216 -0
  83. package/src/arkweb_script.cj +142 -142
  84. package/src/capability.cj +409 -409
  85. package/src/clipboard_bridge.cj +178 -0
  86. package/src/dialog_spec.cj +79 -0
  87. package/src/dragdrop.cj +26 -26
  88. package/src/embedded_assets.cj +98 -98
  89. package/src/host.cj +752 -607
  90. package/src/host_arkweb.cj +924 -778
  91. package/src/host_webkit.cj +663 -570
  92. package/src/host_webview2.cj +663 -570
  93. package/src/hotkey.cj +66 -66
  94. package/src/ipc_hub.cj +401 -337
  95. package/src/ipc_message.cj +286 -286
  96. package/src/launch.cj +62 -62
  97. package/src/logging.cj +127 -127
  98. package/src/menu.cj +197 -197
  99. package/src/platform/platform_app_context.cj +77 -77
  100. package/src/platform/platform_assets.cj +33 -33
  101. package/src/platform/platform_discover.cj +176 -176
  102. package/src/platform/platform_env.cj +29 -29
  103. package/src/platform/platform_feature.cj +31 -31
  104. package/src/platform/platform_page_source.cj +87 -87
  105. package/src/platform/platform_target_info.cj +32 -32
  106. package/src/platform_host_provider.cj +87 -87
  107. package/src/plugin.cj +333 -333
  108. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  109. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  110. package/src/plugins/fs/plugin_fs.cj +105 -1
  111. package/src/plugins/http/http_backend.cj +131 -0
  112. package/src/plugins/http/http_desktop.cj +195 -0
  113. package/src/plugins/http/http_ohos.cj +204 -0
  114. package/src/plugins/http/plugin_http.cj +310 -459
  115. package/src/plugins/log/plugin_log.cj +105 -105
  116. package/src/plugins/menu/plugin_menu.cj +158 -158
  117. package/src/plugins/notification/plugin_notification.cj +154 -154
  118. package/src/plugins/serial/plugin_serial.cj +458 -454
  119. package/src/plugins/serial/serial_bridge.cj +194 -194
  120. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  121. package/src/plugins/serial/serial_ohos.cj +529 -0
  122. package/src/plugins/shell/plugin_shell.cj +258 -258
  123. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  124. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  125. package/src/plugins/ws/plugin_ws.cj +488 -470
  126. package/src/process_hidden.cj +174 -174
  127. package/src/scope.cj +391 -391
  128. package/src/shell_event.cj +69 -69
  129. package/src/tests/app_spec_test.cj +130 -127
  130. package/src/tests/capability_test.cj +243 -243
  131. package/src/tests/dialog_spec_test.cj +73 -0
  132. package/src/tests/fake_host.cj +201 -201
  133. package/src/tests/ipc_hub_test.cj +563 -487
  134. package/src/tests/logging_test.cj +62 -62
  135. package/src/tests/platform_contract_test.cj +264 -251
  136. package/src/tests/plugin_clipboard_test.cj +222 -222
  137. package/src/tests/plugin_dialog_test.cj +237 -237
  138. package/src/tests/plugin_fs_test.cj +207 -0
  139. package/src/tests/plugin_http_test.cj +124 -124
  140. package/src/tests/plugin_notification_test.cj +205 -205
  141. package/src/tests/plugin_serial_test.cj +48 -0
  142. package/src/tests/plugin_test.cj +605 -585
  143. package/src/tests/plugin_ws_test.cj +40 -40
  144. package/src/tests/process_hidden_test.cj +129 -129
  145. package/src/tests/scope_test.cj +350 -350
  146. package/src/tests/system_host_test.cj +99 -81
  147. package/src/tests/urlutil_test.cj +53 -53
  148. package/src/tests/version_test.cj +75 -75
  149. package/src/tests/window_registry_test.cj +123 -123
  150. package/src/tray.cj +90 -90
  151. package/src/urlutil.cj +159 -159
  152. package/src/version.cj +44 -44
  153. package/src/window.cj +51 -51
  154. package/src/window_registry.cj +116 -116
  155. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  156. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  157. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,570 +1,663 @@
1
- package cjTauri
2
-
3
- // Linux 侧才用得上(类整体门禁在 Linux):加同款门禁,免得 Windows 构建把这条当「未使用 import」清掉
4
- // (host_webview2.cj 是镜像写法。清掉它 Linux 侧当场编炸:`error: undeclared identifier 'std'`,
5
- // 2026-10-13 实测;48fe433、1747075 已踩过两次——AGENTS §4 同条)
6
- @When[os == "Linux" && env != "ohos"]
7
- import std.collection.*
8
-
9
- // 页面来源契约(load(source) 收敛,RFC §4.3):同款门禁,免得别的目标把这条当未使用清掉
10
- @When[os == "Linux" && env != "ohos"]
11
- import cjTauri.platform.*
12
-
13
- /**
14
- * C 桥 FFI 声明(native/libcjtbridge.so)
15
- * 所有 GTK/WebKit 调用在 C 桥的原生 pthread 内执行(见 native/bridge_linux.c 头注释)。
16
- *
17
- * 句柄制(批次 1):宿主状态在 C 侧按句柄隔离,导出一律首参收句柄(`cj_host*`)。
18
- * 句柄内存由 C 侧分配/回收(create 分配、destroy 释放)——跨 FFI 绝不「这边分配那边释放」
19
- * (Windows 上 CRT 与仓颉运行时不是同一个分配器);生命周期约定见
20
- * docs/架构演进-多平台与多窗口.md §6.1。
21
- */
22
- @When[os == "Linux" && env != "ohos"]
23
- @C
24
- foreign func cj_bridge_create(): CPointer<Unit>
25
-
26
- /**
27
- * 释放宿主编:对空指针安全、幂等;调用后该句柄不可再用
28
- */
29
- @When[os == "Linux" && env != "ohos"]
30
- @C
31
- foreign func cj_bridge_destroy(h: CPointer<Unit>): Unit
32
-
33
- @When[os == "Linux" && env != "ohos"]
34
- @C
35
- foreign func cj_bridge_init(h: CPointer<Unit>, m: CFunc<(CString) -> Unit>, d: CFunc<(CPointer<Unit>) -> Unit>): Unit
36
-
37
- @When[os == "Linux" && env != "ohos"]
38
- @C
39
- foreign func cj_bridge_start(h: CPointer<Unit>, html: CString): Unit
40
-
41
- @When[os == "Linux" && env != "ohos"]
42
- @C
43
- foreign func cj_bridge_load_url(h: CPointer<Unit>, url: CString): Unit
44
-
45
- /**
46
- * 注册一段预执行脚本(插件 jsShim 等):须在 cj_bridge_start 之前调用,桥在 document-start 注入
47
- */
48
- @When[os == "Linux" && env != "ohos"]
49
- @C
50
- foreign func cj_bridge_add_init_script(h: CPointer<Unit>, js: CString): Unit
51
-
52
- @When[os == "Linux" && env != "ohos"]
53
- @C
54
- foreign func cj_bridge_reload(h: CPointer<Unit>): Unit
55
-
56
- @When[os == "Linux" && env != "ohos"]
57
- @C
58
- foreign func cj_bridge_set_window(h: CPointer<Unit>, title: CString, width: Int32, height: Int32): Unit
59
-
60
- @When[os == "Linux" && env != "ohos"]
61
- @C
62
- foreign func cj_bridge_set_devtools(h: CPointer<Unit>, enabled: Int32): Unit
63
-
64
- @When[os == "Linux" && env != "ohos"]
65
- @C
66
- foreign func cj_bridge_set_icon(h: CPointer<Unit>, path: CString): Unit
67
-
68
- @When[os == "Linux" && env != "ohos"]
69
- @C
70
- foreign func cj_bridge_open_devtools(h: CPointer<Unit>): Unit
71
-
72
- @When[os == "Linux" && env != "ohos"]
73
- @C
74
- foreign func cj_bridge_set_dialog_callback(h: CPointer<Unit>, cb: CFunc<(CPointer<Unit>, CString) -> Unit>): Unit
75
-
76
- /**
77
- * 原生对话框(阻塞式):kind 0=打开文件 1=保存文件 2=info 3=warning 4=error 5=confirm。
78
- * 返回 1 = 用户确认;文件类选中路径经 cj_bridge_set_dialog_callback 注册的回调送回。
79
- */
80
- @When[os == "Linux" && env != "ohos"]
81
- @C
82
- foreign func cj_bridge_show_dialog(h: CPointer<Unit>, kind: Int32, title: CString, message: CString,
83
- filter: CString): Int32
84
-
85
- @When[os == "Linux" && env != "ohos"]
86
- @C
87
- foreign func cj_bridge_run_js(h: CPointer<Unit>, js: CString): Unit
88
-
89
- @When[os == "Linux" && env != "ohos"]
90
- @C
91
- foreign func cj_bridge_quit(h: CPointer<Unit>): Unit
92
-
93
- @When[os == "Linux" && env != "ohos"]
94
- @C
95
- foreign func cj_bridge_is_ready(h: CPointer<Unit>): Int32
96
-
97
- @When[os == "Linux" && env != "ohos"]
98
- @C
99
- foreign func cj_bridge_should_quit(h: CPointer<Unit>): Int32
100
-
101
- /**
102
- * 菜单栏(RFC-002 §5.4 的线路文本):空串 = 清空。未就绪时桥只记下,建窗时一次建好。
103
- */
104
- @When[os == "Linux" && env != "ohos"]
105
- @C
106
- foreign func cj_bridge_set_menu(h: CPointer<Unit>, wire: CString): Unit
107
-
108
- /**
109
- * 运行期改单个菜单项的状态(按 id 找;找不到只记日志)
110
- */
111
- @When[os == "Linux" && env != "ohos"]
112
- @C
113
- foreign func cj_bridge_set_menu_item_state(h: CPointer<Unit>, id: CString, enabled: Int32,
114
- checked: Int32): Unit
115
-
116
- /**
117
- * 窗口顶部副标题(与菜单栏同一行的右侧)。返回 0 = 已提交、-3 = 宿主未就绪、-4 = 平台没有这块
118
- */
119
- @When[os == "Linux" && env != "ohos"]
120
- @C
121
- foreign func cj_bridge_set_titlebar_subtitle(h: CPointer<Unit>, text: CString): Int32
122
-
123
- /** 拖放策略(RFC-002 §5):enabled=0 平台不注册 drop target(默认关) */
124
- @When[os == "Linux" && env != "ohos"]
125
- @C
126
- foreign func cj_bridge_set_drag_drop(h: CPointer<Unit>, enabled: Int32, files: Int32): Unit
127
-
128
- /**
129
- * 全局快捷键(RFC-003 第 5 期):wire 一行一条 `<accel>:<id>`。
130
- * Linux 走 X11 grab(可选编译:桥没编 X11 支持时打一行 stderr 忽略)。
131
- */
132
- @When[os == "Linux" && env != "ohos"]
133
- @C
134
- foreign func cj_bridge_set_global_hotkeys(h: CPointer<Unit>, wire: CString): Unit
135
-
136
- /**
137
- * 托盘(RFC-002 §7.C):wire 见 TrayModel.toWire(),空串 = 移除。
138
- * Linux 走 libayatana-appindicator3(可选编译:桥没编托盘支持时打一行 stderr 忽略)。
139
- */
140
- @When[os == "Linux" && env != "ohos"]
141
- @C
142
- foreign func cj_bridge_set_tray(h: CPointer<Unit>, wire: CString): Unit
143
-
144
- /**
145
- * 窗口可见性(`show()` / `hide()` 的 C 侧落点):start 之前调只记 pending,就绪后投 GTK 线程执行
146
- */
147
- @When[os == "Linux" && env != "ohos"]
148
- @C
149
- foreign func cj_bridge_set_window_visible(h: CPointer<Unit>, visible: Int32): Unit
150
-
151
- /**
152
- * 最小化到托盘策略位(须在 start 之前调):GTK 侧据此把 window-state-event 里的 iconify 改写成 hide
153
- */
154
- @When[os == "Linux" && env != "ohos"]
155
- @C
156
- foreign func cj_bridge_set_minimize_to_tray(h: CPointer<Unit>, enabled: Int32): Unit
157
-
158
- /**
159
- * shell 事件回调(菜单点击等):首参是宿主句柄,多窗口靠它分辨事件属于哪个窗口
160
- */
161
- @When[os == "Linux" && env != "ohos"]
162
- @C
163
- foreign func cj_bridge_set_shell_callback(h: CPointer<Unit>,
164
- cb: CFunc<(CPointer<Unit>, CString) -> Unit>): Unit
165
-
166
- /**
167
- * 宿主能力位(CJ_CAP_* 的按位或):未 init 返回 0
168
- */
169
- @When[os == "Linux" && env != "ohos"]
170
- @C
171
- foreign func cj_bridge_host_capabilities(h: CPointer<Unit>): Int32
172
-
173
- /**
174
- * 句柄同一性判定(返回 1 = 同一个宿主):仓颉侧 CPointer 不能比较,判定交回 C 侧
175
- */
176
- @When[os == "Linux" && env != "ohos"]
177
- @C
178
- foreign func cj_bridge_host_eq(a: CPointer<Unit>, b: CPointer<Unit>): Int32
179
-
180
- /**
181
- * Linux WebKit 宿主实现:通过 C 桥驱动
182
- */
183
- @When[os == "Linux" && env != "ohos"]
184
- public class WebKitHost <: WebViewHost {
185
- // 消息回调:GTK 线程(原生 pthread)→ 仓颉
186
- public var onMessage: (String) -> Unit = { _ => }
187
- // 销毁回调(应用经 setDestroyHandler 装配;由 C 桥按句柄认领,多窗口不串)
188
- public var onDestroy: () -> Unit = { => }
189
- // 本窗口文件对话框的结果:C 桥按句柄经路由回调写入(两窗各写各的,不许串台)
190
- private var dialogResult: String = ""
191
- // 本窗口的 shell 事件(菜单点击等)回调:TauriApp 装配时经 setShellHandler 覆盖它做分发,
192
- // 未覆盖时只留一行 stderr——菜单点了没人管,至少能在日志里看见。
193
- public var shellHandler: (String) -> Unit = { json =>
194
- eprintln("[cj-tauri] shell event (no handler): ${json}")
195
- }
196
- // 窗口配置(标题 / 尺寸),start() 前由 TauriApp 注入
197
- private var config: WindowConfig = WindowConfig()
198
- // 预执行脚本(插件 jsShim 等):start 前收集,prepareHost 一次性交给桥,桥在 document-start 注入
199
- private var initScripts = ArrayList<String>()
200
- // 宿主编(C 侧分配):空指针 = 尚未创建;dispose() 后置空
201
- private var handle: CPointer<Unit> = CPointer<Unit>()
202
- // 本窗口的 label(字段名不叫 label:接口有同名方法)。单窗口阶段是常量,
203
- // 与 TauriApp 注册的 WindowRecord.label 一致;多窗口时由构造参数给定。
204
- private let windowLabel: String
205
- // 资产解析器:装配层在 load 前注入(None = 按 cwd 相对路径读盘,与批次 1 行为等价)
206
- private var assetResolver: Option<AssetResolver> = None
207
-
208
- public init() {
209
- this.windowLabel = DEFAULT_WINDOW_LABEL
210
- }
211
-
212
- /**
213
- * 带 label 构造(多窗口):label 必须与 TauriApp 注册表里登记的那个一致,
214
- * 页面侧的 `window.__CJ_TAURI_LABEL__` 也取同一个值(由 TauriApp 注入)。
215
- */
216
- public init(label: String) {
217
- this.windowLabel = label
218
- }
219
-
220
- public func label(): String {
221
- return this.windowLabel
222
- }
223
-
224
- public func setMessageHandler(handler: (String) -> Unit): Unit {
225
- this.onMessage = handler
226
- }
227
-
228
- public func setWindowConfig(config: WindowConfig): Unit {
229
- this.config = config
230
- }
231
-
232
- public func setDevTools(enabled: Bool): Unit {
233
- this.config.devTools = enabled
234
- }
235
-
236
- public func addInitScript(js: String): Unit {
237
- // 空串忽略(插件没写 shim 时不必占位,桥侧也会再挡一次)
238
- if (js.size > 0) {
239
- this.initScripts.add(js)
240
- }
241
- }
242
-
243
- public func openDevTools(): Unit {
244
- unsafe {
245
- cj_bridge_open_devtools(this.handle)
246
- }
247
- }
248
-
249
- public func setMenu(wire: String): Unit {
250
- // 句柄先建出来:setMenu 允许在 start() 之前调——文本由桥记着,建窗时一次建好
251
- this.ensureHandle()
252
- unsafe {
253
- cj_bridge_set_menu(this.handle, LibC.mallocCString(wire))
254
- }
255
- }
256
-
257
- public func setMenuItemState(id: String, enabled: Bool, checked: Bool): Unit {
258
- if (this.handle.isNull()) {
259
- // 还没建宿主就谈不上「改状态」:静默返回,不拿空句柄去 FFI
260
- return
261
- }
262
- var e: Int32 = 0
263
- var c: Int32 = 0
264
- if (enabled) {
265
- e = 1
266
- }
267
- if (checked) {
268
- c = 1
269
- }
270
- unsafe {
271
- cj_bridge_set_menu_item_state(this.handle, LibC.mallocCString(id), e, c)
272
- }
273
- }
274
-
275
- public func setTitlebarSubtitle(text: String): Unit {
276
- if (this.handle.isNull()) {
277
- // 运行期能力:建窗前没有落点(见接口注释),不拿空句柄去 FFI
278
- return
279
- }
280
- unsafe {
281
- let rc = cj_bridge_set_titlebar_subtitle(this.handle, LibC.mallocCString(text))
282
- if (rc != 0) {
283
- // 能力位说支持、这次却没写进去:留一行凭证(宿主未就绪 / 文本超 1024 字节)
284
- eprintln("[cj-tauri] titlebar subtitle rejected: rc=${rc}")
285
- }
286
- }
287
- }
288
-
289
- public func setDragDropPolicy(enabled: Bool, files: Bool): Unit {
290
- // 与 setMenu 同形:允许在 start() 之前调——策略由桥记 pending,建 view 时挂 drag dest
291
- this.ensureHandle()
292
- var en: Int32 = 0
293
- var fi: Int32 = 0
294
- if (enabled) {
295
- en = 1
296
- }
297
- if (files) {
298
- fi = 1
299
- }
300
- unsafe {
301
- cj_bridge_set_drag_drop(this.handle, en, fi)
302
- }
303
- }
304
-
305
- public func setGlobalHotkeys(wire: String): Unit {
306
- // 与 setMenu 同形:允许在 start() 之前调——线路由桥记 pending,宿主线程起来后 grab
307
- this.ensureHandle()
308
- unsafe {
309
- cj_bridge_set_global_hotkeys(this.handle, LibC.mallocCString(wire))
310
- }
311
- }
312
-
313
- public func setTray(wire: String): Unit {
314
- // 与 setMenu 同形:允许在 start() 之前调——线路由桥记 pending,宿主线程装配时创建
315
- this.ensureHandle()
316
- unsafe {
317
- cj_bridge_set_tray(this.handle, LibC.mallocCString(wire))
318
- }
319
- }
320
-
321
- public func setWindowVisible(visible: Bool): Unit {
322
- // 与 setTray 同形:允许在 start() 之前调(桥记 pending,建窗时定稿);就绪后投 GTK 线程执行
323
- this.ensureHandle()
324
- var v: Int32 = 0
325
- if (visible) {
326
- v = 1
327
- }
328
- unsafe {
329
- cj_bridge_set_window_visible(this.handle, v)
330
- }
331
- }
332
-
333
- public func setMinimizeToTray(enabled: Bool): Unit {
334
- // 策略位:GTK 侧要在建窗时连 window-state-event 信号,所以只在 start() 之前调才有意义
335
- this.ensureHandle()
336
- var e: Int32 = 0
337
- if (enabled) {
338
- e = 1
339
- }
340
- unsafe {
341
- cj_bridge_set_minimize_to_tray(this.handle, e)
342
- }
343
- }
344
-
345
- public func hostCapabilities(): Int32 {
346
- if (this.handle.isNull()) {
347
- return 0
348
- }
349
- var caps: Int32 = 0
350
- unsafe {
351
- caps = cj_bridge_host_capabilities(this.handle)
352
- }
353
- return caps
354
- }
355
-
356
- public func setShellHandler(handler: (String) -> Unit): Unit {
357
- this.shellHandler = handler
358
- }
359
-
360
- public func setDestroyHandler(handler: () -> Unit): Unit {
361
- this.onDestroy = handler
362
- }
363
-
364
- public func showFileDialog(save: Bool, title: String, filter: String): String {
365
- return this.showFileDialogPreset(save, title, filter, "")
366
- }
367
-
368
- public func showFileDialogPreset(save: Bool, title: String, filter: String,
369
- defaultName: String): String {
370
- // GTK 轮未接预置文件名(自动化取证只在 Windows 侧做):如实降级为普通文件框
371
- let _ = defaultName
372
- var kind: Int32 = 0
373
- if (save) {
374
- kind = 1
375
- }
376
- var ok: Int32 = 0
377
- unsafe {
378
- ok = cj_bridge_show_dialog(this.handle, kind, LibC.mallocCString(title),
379
- LibC.mallocCString(""), LibC.mallocCString(filter))
380
- }
381
- if (ok == 0) {
382
- // 取消 / 宿主未就绪:上游按「空路径」处理,不再读结果位
383
- return ""
384
- }
385
- return this.dialogResult
386
- }
387
-
388
- public func showFolderDialog(title: String): String {
389
- var ok: Int32 = 0
390
- unsafe {
391
- // 选目录:kind=6(与文件类 / 消息类并列,见 bridge_core.h 的 dlg_req);
392
- // message / filter 不参与,方向框也没有「默认文件名」的概念
393
- ok = cj_bridge_show_dialog(this.handle, 6, LibC.mallocCString(title),
394
- LibC.mallocCString(""), LibC.mallocCString(""))
395
- }
396
- if (ok == 0) {
397
- // 取消 / 宿主未就绪:上游按「空路径」处理
398
- return ""
399
- }
400
- return this.dialogResult
401
- }
402
-
403
- public func showMessageDialog(kind: String, title: String, message: String): Bool {
404
- var ok: Int32 = 0
405
- unsafe {
406
- ok = cj_bridge_show_dialog(this.handle, dialogKindCode(kind), LibC.mallocCString(title),
407
- LibC.mallocCString(message), LibC.mallocCString(""))
408
- }
409
- return ok != 0
410
- }
411
-
412
- public func clipboardWriteText(text: String): Bool {
413
- var rc: Int32 = -1
414
- unsafe {
415
- rc = cj_bridge_clipboard_write_text(this.handle, LibC.mallocCString(text))
416
- }
417
- if (rc != 0) {
418
- eprintln("[cj-tauri] clipboard write failed: code=${rc}")
419
- }
420
- return rc == 0
421
- }
422
-
423
- public func clipboardReadText(): Option<String> {
424
- let cs = unsafe { cj_bridge_clipboard_read_text(this.handle) }
425
- if (cs.isNull()) {
426
- return None<String> // 空 / 非文本 / 平台失败,协议层不区分
427
- }
428
- let text = cs.toString()
429
- unsafe {
430
- cj_bridge_clipboard_free(cs) // 归还缓冲:malloc 家族分配,必须走桥的 free
431
- }
432
- return Some(text)
433
- }
434
-
435
- public func notify(title: String, body: String): Bool {
436
- var rc: Int32 = -1
437
- unsafe {
438
- rc = cj_bridge_notification_notify(this.handle, LibC.mallocCString(title),
439
- LibC.mallocCString(body))
440
- }
441
- if (rc != 0) {
442
- // -4 = 本平台未实现:这是静态事实,stderr 一行说清,不算异常
443
- eprintln("[cj-tauri] notification failed: code=${rc}")
444
- }
445
- return rc == 0
446
- }
447
-
448
- /**
449
- * 创建宿主编(幂等):首次启动时分配,dispose() 后置空
450
- */
451
- private func ensureHandle(): Unit {
452
- if (this.handle.isNull()) {
453
- unsafe {
454
- this.handle = cj_bridge_create()
455
- }
456
- }
457
- }
458
-
459
- /**
460
- * 启动前的共同装配:宿主编、回调槽位、窗口配置、图标
461
- */
462
- private func prepareHost(): Unit {
463
- this.ensureHandle()
464
- HostGlobals.onMessage = this.onMessage
465
- // 宿主级路由:「本句柄 → 本实例的三个回调」登记一条(表式,不覆盖别的窗口)
466
- HostGlobals.registerHostRoute(this.handle, { json => this.shellHandler(json) },
467
- { => this.onDestroy() },
468
- { path => this.dialogResult = path })
469
- var devTools: Int32 = 0
470
- if (this.config.devTools) {
471
- devTools = 1
472
- }
473
- unsafe {
474
- cj_bridge_init(this.handle, hostOnMessage, hostOnDestroy)
475
- // shell 事件回调(菜单点击等):与消息回调同一套路,多一个首参用来分辨窗口
476
- cj_bridge_set_shell_callback(this.handle, shellOnEvent)
477
- // 原生对话框的结果回调:与消息回调同一套路,多一个首参(结果只落发起这次对话框的窗口)
478
- cj_bridge_set_dialog_callback(this.handle, dialogOnResult)
479
- cj_bridge_set_window(this.handle, LibC.mallocCString(this.config.title),
480
- Int32(this.config.width), Int32(this.config.height))
481
- cj_bridge_set_devtools(this.handle, devTools)
482
- // 窗口图标:空串表示系统默认图标
483
- cj_bridge_set_icon(this.handle, LibC.mallocCString(this.config.iconPath))
484
- // 预执行脚本(插件 jsShim 等):与 BRIDGE_JS 同一条 document-start 通道,按注册顺序排在桥之后
485
- for (js in this.initScripts) {
486
- cj_bridge_add_init_script(this.handle, LibC.mallocCString(js))
487
- }
488
- }
489
- }
490
-
491
- /**
492
- * 加载页面(唯一入口,RFC §4.3):Inline/Url/DevServer 直接映射;
493
- * Asset 经注入的 resolver 读盘,未注入时按 cwd 相对路径读(批次 1 的桌面行为)
494
- */
495
- public func load(source: PageSource): Unit {
496
- match (resolvePage(source, this.assetResolver)) {
497
- case StartHtml(html) => this.start(html)
498
- case StartUrl(url) => this.startUrl(url)
499
- }
500
- }
501
-
502
- /** 注入资产解析器(装配层在 load 前调用) */
503
- public func setAssetResolver(resolver: Option<AssetResolver>): Unit {
504
- this.assetResolver = resolver
505
- }
506
-
507
- public func start(html: String): Unit {
508
- this.prepareHost()
509
- unsafe {
510
- cj_bridge_start(this.handle, LibC.mallocCString(html))
511
- }
512
- }
513
-
514
- public func startUrl(url: String): Unit {
515
- this.prepareHost()
516
- unsafe {
517
- cj_bridge_load_url(this.handle, LibC.mallocCString(url))
518
- // 空串表示「页面由 cj_bridge_load_url 指定」(同一次启动里 URL 优先)
519
- cj_bridge_start(this.handle, LibC.mallocCString(""))
520
- }
521
- }
522
-
523
- public func reload(): Unit {
524
- unsafe {
525
- cj_bridge_reload(this.handle)
526
- }
527
- }
528
-
529
- public func runJs(js: String): Unit {
530
- unsafe {
531
- cj_bridge_run_js(this.handle, LibC.mallocCString(js))
532
- }
533
- }
534
-
535
- public func quit(): Unit {
536
- unsafe {
537
- cj_bridge_quit(this.handle)
538
- }
539
- }
540
-
541
- public func shouldQuit(): Bool {
542
- unsafe {
543
- cj_bridge_should_quit(this.handle) != 0
544
- }
545
- }
546
-
547
- /**
548
- * 释放宿主编(幂等,未创建过则是空操作):事件循环退出后调用一次,之后句柄置空、本实例不可再启动
549
- */
550
- public func dispose(): Unit {
551
- if (this.handle.isNull()) {
552
- return
553
- }
554
- unsafe {
555
- cj_bridge_destroy(this.handle)
556
- }
557
- this.handle = CPointer<Unit>()
558
- }
559
- }
560
-
561
- // C 回调中转:C 桥线程 → 仓颉闭包(经全局槽位)
562
- @When[os == "Linux" && env != "ohos"]
563
- @C
564
- func hostOnMessage(json: CString): Unit {
565
- try {
566
- HostGlobals.onMessage(json.toString())
567
- } catch (e: Exception) {
568
- eprintln("[cj-tauri] host callback error: ${e}")
569
- }
570
- }
1
+ package cjTauri
2
+
3
+ // Linux 侧才用得上(类整体门禁在 Linux):加同款门禁,免得 Windows 构建把这条当「未使用 import」清掉
4
+ // (host_webview2.cj 是镜像写法。清掉它 Linux 侧当场编炸:`error: undeclared identifier 'std'`,
5
+ // 2026-10-13 实测;48fe433、1747075 已踩过两次——AGENTS §4 同条)
6
+ @When[os == "Linux" && env != "ohos"]
7
+ import std.collection.*
8
+
9
+ // 页面来源契约(load(source) 收敛,RFC §4.3):同款门禁,免得别的目标把这条当未使用清掉
10
+ @When[os == "Linux" && env != "ohos"]
11
+ import cjTauri.platform.*
12
+
13
+ /**
14
+ * C 桥 FFI 声明(native/libcjtbridge.so)
15
+ * 所有 GTK/WebKit 调用在 C 桥的原生 pthread 内执行(见 native/bridge_linux.c 头注释)。
16
+ *
17
+ * 句柄制(批次 1):宿主状态在 C 侧按句柄隔离,导出一律首参收句柄(`cj_host*`)。
18
+ * 句柄内存由 C 侧分配/回收(create 分配、destroy 释放)——跨 FFI 绝不「这边分配那边释放」
19
+ * (Windows 上 CRT 与仓颉运行时不是同一个分配器);生命周期约定见
20
+ * docs/架构演进-多平台与多窗口.md §6.1。
21
+ */
22
+ @When[os == "Linux" && env != "ohos"]
23
+ @C
24
+ foreign func cj_bridge_create(): CPointer<Unit>
25
+
26
+ /**
27
+ * 释放宿主编:对空指针安全、幂等;调用后该句柄不可再用
28
+ */
29
+ @When[os == "Linux" && env != "ohos"]
30
+ @C
31
+ foreign func cj_bridge_destroy(h: CPointer<Unit>): Unit
32
+
33
+ @When[os == "Linux" && env != "ohos"]
34
+ @C
35
+ foreign func cj_bridge_init(h: CPointer<Unit>, m: CFunc<(CString) -> Unit>, d: CFunc<(CPointer<Unit>) -> Unit>): Unit
36
+
37
+ @When[os == "Linux" && env != "ohos"]
38
+ @C
39
+ foreign func cj_bridge_start(h: CPointer<Unit>, html: CString): Unit
40
+
41
+ @When[os == "Linux" && env != "ohos"]
42
+ @C
43
+ foreign func cj_bridge_load_url(h: CPointer<Unit>, url: CString): Unit
44
+
45
+ /**
46
+ * 注册一段预执行脚本(插件 jsShim 等):须在 cj_bridge_start 之前调用,桥在 document-start 注入
47
+ */
48
+ @When[os == "Linux" && env != "ohos"]
49
+ @C
50
+ foreign func cj_bridge_add_init_script(h: CPointer<Unit>, js: CString): Unit
51
+
52
+ @When[os == "Linux" && env != "ohos"]
53
+ @C
54
+ foreign func cj_bridge_reload(h: CPointer<Unit>): Unit
55
+
56
+ @When[os == "Linux" && env != "ohos"]
57
+ @C
58
+ foreign func cj_bridge_set_window(h: CPointer<Unit>, title: CString, width: Int32, height: Int32): Unit
59
+
60
+ @When[os == "Linux" && env != "ohos"]
61
+ @C
62
+ foreign func cj_bridge_set_devtools(h: CPointer<Unit>, enabled: Int32): Unit
63
+
64
+ @When[os == "Linux" && env != "ohos"]
65
+ @C
66
+ foreign func cj_bridge_set_icon(h: CPointer<Unit>, path: CString): Unit
67
+
68
+ @When[os == "Linux" && env != "ohos"]
69
+ @C
70
+ foreign func cj_bridge_open_devtools(h: CPointer<Unit>): Unit
71
+
72
+ @When[os == "Linux" && env != "ohos"]
73
+ @C
74
+ foreign func cj_bridge_set_dialog_callback(h: CPointer<Unit>, cb: CFunc<(CPointer<Unit>, CString) -> Unit>): Unit
75
+
76
+ /**
77
+ * 原生对话框(阻塞式):kind 0=打开文件 1=保存文件 2=info 3=warning 4=error 5=confirm。
78
+ * 返回 1 = 用户确认;文件类选中路径经 cj_bridge_set_dialog_callback 注册的回调送回。
79
+ */
80
+ @When[os == "Linux" && env != "ohos"]
81
+ @C
82
+ foreign func cj_bridge_show_dialog(h: CPointer<Unit>, kind: Int32, title: CString, message: CString,
83
+ filter: CString): Int32
84
+
85
+ @When[os == "Linux" && env != "ohos"]
86
+ @C
87
+ foreign func cj_bridge_run_js(h: CPointer<Unit>, js: CString): Unit
88
+
89
+ @When[os == "Linux" && env != "ohos"]
90
+ @C
91
+ foreign func cj_bridge_quit(h: CPointer<Unit>): Unit
92
+
93
+ @When[os == "Linux" && env != "ohos"]
94
+ @C
95
+ foreign func cj_bridge_is_ready(h: CPointer<Unit>): Int32
96
+
97
+ @When[os == "Linux" && env != "ohos"]
98
+ @C
99
+ foreign func cj_bridge_should_quit(h: CPointer<Unit>): Int32
100
+
101
+ /**
102
+ * 菜单栏(RFC-002 §5.4 的线路文本):空串 = 清空。未就绪时桥只记下,建窗时一次建好。
103
+ */
104
+ @When[os == "Linux" && env != "ohos"]
105
+ @C
106
+ foreign func cj_bridge_set_menu(h: CPointer<Unit>, wire: CString): Unit
107
+
108
+ /**
109
+ * 运行期改单个菜单项的状态(按 id 找;找不到只记日志)
110
+ */
111
+ @When[os == "Linux" && env != "ohos"]
112
+ @C
113
+ foreign func cj_bridge_set_menu_item_state(h: CPointer<Unit>, id: CString, enabled: Int32,
114
+ checked: Int32): Unit
115
+
116
+ /**
117
+ * 窗口顶部副标题(与菜单栏同一行的右侧)。返回 0 = 已提交、-3 = 宿主未就绪、-4 = 平台没有这块
118
+ */
119
+ @When[os == "Linux" && env != "ohos"]
120
+ @C
121
+ foreign func cj_bridge_set_titlebar_subtitle(h: CPointer<Unit>, text: CString): Int32
122
+
123
+ /** 拖放策略(RFC-002 §5):enabled=0 平台不注册 drop target(默认关) */
124
+ @When[os == "Linux" && env != "ohos"]
125
+ @C
126
+ foreign func cj_bridge_set_drag_drop(h: CPointer<Unit>, enabled: Int32, files: Int32): Unit
127
+
128
+ /**
129
+ * 同窗内嵌视图(PLT-006):create 返回视图句柄(> 0 有效,0 = 平台未就绪 / 不支持)。
130
+ * 五条都只是「投递」——桥把动作 g_idle_add 到 GTK 线程执行,句柄当场返回,
131
+ * 所以 create 成功不代表内容已经出来(视图是随后建的)。
132
+ */
133
+ @When[os == "Linux" && env != "ohos"]
134
+ @C
135
+ foreign func cj_bridge_view_create(h: CPointer<Unit>, url: CString,
136
+ x: Int32, y: Int32, width: Int32, height: Int32): Int64
137
+
138
+ @When[os == "Linux" && env != "ohos"]
139
+ @C
140
+ foreign func cj_bridge_view_set_visible(h: CPointer<Unit>, view: Int64, visible: Int32): Unit
141
+
142
+ @When[os == "Linux" && env != "ohos"]
143
+ @C
144
+ foreign func cj_bridge_view_set_bounds(h: CPointer<Unit>, view: Int64,
145
+ x: Int32, y: Int32, width: Int32, height: Int32): Unit
146
+
147
+ @When[os == "Linux" && env != "ohos"]
148
+ @C
149
+ foreign func cj_bridge_view_navigate(h: CPointer<Unit>, view: Int64, url: CString): Unit
150
+
151
+ @When[os == "Linux" && env != "ohos"]
152
+ @C
153
+ foreign func cj_bridge_view_destroy(h: CPointer<Unit>, view: Int64): Unit
154
+
155
+ /**
156
+ * 全局快捷键(RFC-003 第 5 期):wire 一行一条 `<accel>:<id>`。
157
+ * Linux 走 X11 grab(可选编译:桥没编 X11 支持时打一行 stderr 忽略)。
158
+ */
159
+ @When[os == "Linux" && env != "ohos"]
160
+ @C
161
+ foreign func cj_bridge_set_global_hotkeys(h: CPointer<Unit>, wire: CString): Unit
162
+
163
+ /**
164
+ * 托盘(RFC-002 §7.C):wire 见 TrayModel.toWire(),空串 = 移除。
165
+ * Linux 走 libayatana-appindicator3(可选编译:桥没编托盘支持时打一行 stderr 忽略)。
166
+ */
167
+ @When[os == "Linux" && env != "ohos"]
168
+ @C
169
+ foreign func cj_bridge_set_tray(h: CPointer<Unit>, wire: CString): Unit
170
+
171
+ /**
172
+ * 窗口可见性(`show()` / `hide()` 的 C 侧落点):start 之前调只记 pending,就绪后投 GTK 线程执行
173
+ */
174
+ @When[os == "Linux" && env != "ohos"]
175
+ @C
176
+ foreign func cj_bridge_set_window_visible(h: CPointer<Unit>, visible: Int32): Unit
177
+
178
+ /**
179
+ * 把 h 挂到 owner 下(transient-for + skip_taskbar_hint):不重复占任务栏、跟随属主显隐
180
+ *
181
+ * 与 Windows 侧同名同签名,上层调用点不分平台(§2 的跨平台约束)。
182
+ */
183
+ @When[os == "Linux" && env != "ohos"]
184
+ @C
185
+ foreign func cj_bridge_set_owner_window(h: CPointer<Unit>, owner: CPointer<Unit>): Unit
186
+
187
+ /**
188
+ * 最小化到托盘策略位(须在 start 之前调):GTK 侧据此把 window-state-event 里的 iconify 改写成 hide
189
+ */
190
+ @When[os == "Linux" && env != "ohos"]
191
+ @C
192
+ foreign func cj_bridge_set_minimize_to_tray(h: CPointer<Unit>, enabled: Int32): Unit
193
+
194
+ /**
195
+ * shell 事件回调(菜单点击等):首参是宿主句柄,多窗口靠它分辨事件属于哪个窗口
196
+ */
197
+ @When[os == "Linux" && env != "ohos"]
198
+ @C
199
+ foreign func cj_bridge_set_shell_callback(h: CPointer<Unit>,
200
+ cb: CFunc<(CPointer<Unit>, CString) -> Unit>): Unit
201
+
202
+ /**
203
+ * 宿主能力位(CJ_CAP_* 的按位或):未 init 返回 0
204
+ */
205
+ @When[os == "Linux" && env != "ohos"]
206
+ @C
207
+ foreign func cj_bridge_host_capabilities(h: CPointer<Unit>): Int32
208
+
209
+ /**
210
+ * 句柄同一性判定(返回 1 = 同一个宿主):仓颉侧 CPointer 不能比较,判定交回 C 侧
211
+ */
212
+ @When[os == "Linux" && env != "ohos"]
213
+ @C
214
+ foreign func cj_bridge_host_eq(a: CPointer<Unit>, b: CPointer<Unit>): Int32
215
+
216
+ /**
217
+ * Linux WebKit 宿主实现:通过 C 桥驱动
218
+ */
219
+ @When[os == "Linux" && env != "ohos"]
220
+ public class WebKitHost <: WebViewHost {
221
+ // 消息回调:GTK 线程(原生 pthread)→ 仓颉
222
+ public var onMessage: (String) -> Unit = { _ => }
223
+ // 销毁回调(应用经 setDestroyHandler 装配;由 C 桥按句柄认领,多窗口不串)
224
+ public var onDestroy: () -> Unit = { => }
225
+ // 本窗口文件对话框的结果:C 桥按句柄经路由回调写入(两窗各写各的,不许串台)
226
+ private var dialogResult: String = ""
227
+ // 本窗口的 shell 事件(菜单点击等)回调:TauriApp 装配时经 setShellHandler 覆盖它做分发,
228
+ // 未覆盖时只留一行 stderr——菜单点了没人管,至少能在日志里看见。
229
+ public var shellHandler: (String) -> Unit = { json =>
230
+ eprintln("[cj-tauri] shell event (no handler): ${json}")
231
+ }
232
+ // 窗口配置(标题 / 尺寸),start() 前由 TauriApp 注入
233
+ private var config: WindowConfig = WindowConfig()
234
+ // 预执行脚本(插件 jsShim 等):start 前收集,prepareHost 一次性交给桥,桥在 document-start 注入
235
+ private var initScripts = ArrayList<String>()
236
+ // 宿主编(C 侧分配):空指针 = 尚未创建;dispose() 后置空
237
+ private var handle: CPointer<Unit> = CPointer<Unit>()
238
+ // 本窗口的 label(字段名不叫 label:接口有同名方法)。单窗口阶段是常量,
239
+ // 与 TauriApp 注册的 WindowRecord.label 一致;多窗口时由构造参数给定。
240
+ private let windowLabel: String
241
+ // 资产解析器:装配层在 load 前注入(None = 按 cwd 相对路径读盘,与批次 1 行为等价)
242
+ private var assetResolver: Option<AssetResolver> = None
243
+
244
+ public init() {
245
+ this.windowLabel = DEFAULT_WINDOW_LABEL
246
+ }
247
+
248
+ /**
249
+ * 带 label 构造(多窗口):label 必须与 TauriApp 注册表里登记的那个一致,
250
+ * 页面侧的 `window.__CJ_TAURI_LABEL__` 也取同一个值(由 TauriApp 注入)。
251
+ */
252
+ public init(label: String) {
253
+ this.windowLabel = label
254
+ }
255
+
256
+ public func label(): String {
257
+ return this.windowLabel
258
+ }
259
+
260
+ public func setMessageHandler(handler: (String) -> Unit): Unit {
261
+ this.onMessage = handler
262
+ }
263
+
264
+ public func setWindowConfig(config: WindowConfig): Unit {
265
+ this.config = config
266
+ }
267
+
268
+ public func setDevTools(enabled: Bool): Unit {
269
+ this.config.devTools = enabled
270
+ }
271
+
272
+ public func addInitScript(js: String): Unit {
273
+ // 空串忽略(插件没写 shim 时不必占位,桥侧也会再挡一次)
274
+ if (js.size > 0) {
275
+ this.initScripts.add(js)
276
+ }
277
+ }
278
+
279
+ public func openDevTools(): Unit {
280
+ unsafe {
281
+ cj_bridge_open_devtools(this.handle)
282
+ }
283
+ }
284
+
285
+ // ===== 同窗内嵌视图(PLT-006):桥侧把动作投回 GTK 线程,这里只是转发 =====
286
+
287
+ public func createEmbeddedView(url: String, x: Int32, y: Int32, width: Int32, height: Int32): Int64 {
288
+ // 窗口没就绪时桥会如实回 0 并打一行 stderr(不是崩溃路径)
289
+ this.ensureHandle()
290
+ unsafe {
291
+ return cj_bridge_view_create(this.handle, LibC.mallocCString(url), x, y, width, height)
292
+ }
293
+ }
294
+
295
+ public func setEmbeddedViewVisible(view: Int64, visible: Bool): Unit {
296
+ var flag: Int32 = 0
297
+ if (visible) {
298
+ flag = 1
299
+ }
300
+ unsafe {
301
+ cj_bridge_view_set_visible(this.handle, view, flag)
302
+ }
303
+ }
304
+
305
+ public func setEmbeddedViewBounds(view: Int64, x: Int32, y: Int32, width: Int32, height: Int32): Unit {
306
+ unsafe {
307
+ cj_bridge_view_set_bounds(this.handle, view, x, y, width, height)
308
+ }
309
+ }
310
+
311
+ public func navigateEmbeddedView(view: Int64, url: String): Unit {
312
+ unsafe {
313
+ cj_bridge_view_navigate(this.handle, view, LibC.mallocCString(url))
314
+ }
315
+ }
316
+
317
+ public func destroyEmbeddedView(view: Int64): Unit {
318
+ unsafe {
319
+ cj_bridge_view_destroy(this.handle, view)
320
+ }
321
+ }
322
+
323
+ public func setMenu(wire: String): Unit {
324
+ // 句柄先建出来:setMenu 允许在 start() 之前调——文本由桥记着,建窗时一次建好
325
+ this.ensureHandle()
326
+ unsafe {
327
+ cj_bridge_set_menu(this.handle, LibC.mallocCString(wire))
328
+ }
329
+ }
330
+
331
+ public func setMenuItemState(id: String, enabled: Bool, checked: Bool): Unit {
332
+ if (this.handle.isNull()) {
333
+ // 还没建宿主就谈不上「改状态」:静默返回,不拿空句柄去 FFI
334
+ return
335
+ }
336
+ var e: Int32 = 0
337
+ var c: Int32 = 0
338
+ if (enabled) {
339
+ e = 1
340
+ }
341
+ if (checked) {
342
+ c = 1
343
+ }
344
+ unsafe {
345
+ cj_bridge_set_menu_item_state(this.handle, LibC.mallocCString(id), e, c)
346
+ }
347
+ }
348
+
349
+ public func setTitlebarSubtitle(text: String): Unit {
350
+ if (this.handle.isNull()) {
351
+ // 运行期能力:建窗前没有落点(见接口注释),不拿空句柄去 FFI
352
+ return
353
+ }
354
+ unsafe {
355
+ let rc = cj_bridge_set_titlebar_subtitle(this.handle, LibC.mallocCString(text))
356
+ if (rc != 0) {
357
+ // 能力位说支持、这次却没写进去:留一行凭证(宿主未就绪 / 文本超 1024 字节)
358
+ eprintln("[cj-tauri] titlebar subtitle rejected: rc=${rc}")
359
+ }
360
+ }
361
+ }
362
+
363
+ public func setDragDropPolicy(enabled: Bool, files: Bool): Unit {
364
+ // 与 setMenu 同形:允许在 start() 之前调——策略由桥记 pending,建 view 时挂 drag dest
365
+ this.ensureHandle()
366
+ var en: Int32 = 0
367
+ var fi: Int32 = 0
368
+ if (enabled) {
369
+ en = 1
370
+ }
371
+ if (files) {
372
+ fi = 1
373
+ }
374
+ unsafe {
375
+ cj_bridge_set_drag_drop(this.handle, en, fi)
376
+ }
377
+ }
378
+
379
+ public func setGlobalHotkeys(wire: String): Unit {
380
+ // 与 setMenu 同形:允许在 start() 之前调——线路由桥记 pending,宿主线程起来后 grab
381
+ this.ensureHandle()
382
+ unsafe {
383
+ cj_bridge_set_global_hotkeys(this.handle, LibC.mallocCString(wire))
384
+ }
385
+ }
386
+
387
+ public func setTray(wire: String): Unit {
388
+ // 与 setMenu 同形:允许在 start() 之前调——线路由桥记 pending,宿主线程装配时创建
389
+ this.ensureHandle()
390
+ unsafe {
391
+ cj_bridge_set_tray(this.handle, LibC.mallocCString(wire))
392
+ }
393
+ }
394
+
395
+ public func setOwnerWindow(ownerLabel: String): Unit {
396
+ if (ownerLabel.size == 0) {
397
+ return
398
+ }
399
+ // 与 setWindowVisible 同形:允许在 start() 之前调(桥记 pending,建窗时定稿)
400
+ this.ensureHandle()
401
+ match (HostGlobals.handleOfLabel(ownerLabel)) {
402
+ case Some(ownerHandle) =>
403
+ unsafe {
404
+ cj_bridge_set_owner_window(this.handle, ownerHandle)
405
+ }
406
+ case None =>
407
+ // 外观修饰失败不该拦着开窗,但要在日志里留凭证
408
+ eprintln("[cj-tauri] setOwnerWindow(${ownerLabel}): 该窗口尚未登记,跳过")
409
+ }
410
+ }
411
+
412
+ public func setWindowVisible(visible: Bool): Unit {
413
+ // 与 setTray 同形:允许在 start() 之前调(桥记 pending,建窗时定稿);就绪后投 GTK 线程执行
414
+ this.ensureHandle()
415
+ var v: Int32 = 0
416
+ if (visible) {
417
+ v = 1
418
+ }
419
+ unsafe {
420
+ cj_bridge_set_window_visible(this.handle, v)
421
+ }
422
+ }
423
+
424
+ public func setMinimizeToTray(enabled: Bool): Unit {
425
+ // 策略位:GTK 侧要在建窗时连 window-state-event 信号,所以只在 start() 之前调才有意义
426
+ this.ensureHandle()
427
+ var e: Int32 = 0
428
+ if (enabled) {
429
+ e = 1
430
+ }
431
+ unsafe {
432
+ cj_bridge_set_minimize_to_tray(this.handle, e)
433
+ }
434
+ }
435
+
436
+ public func hostCapabilities(): Int32 {
437
+ if (this.handle.isNull()) {
438
+ return 0
439
+ }
440
+ var caps: Int32 = 0
441
+ unsafe {
442
+ caps = cj_bridge_host_capabilities(this.handle)
443
+ }
444
+ return caps
445
+ }
446
+
447
+ public func setShellHandler(handler: (String) -> Unit): Unit {
448
+ this.shellHandler = handler
449
+ }
450
+
451
+ public func setDestroyHandler(handler: () -> Unit): Unit {
452
+ this.onDestroy = handler
453
+ }
454
+
455
+ public func showFileDialog(save: Bool, title: String, filter: String): String {
456
+ return this.showFileDialogPreset(save, title, filter, "")
457
+ }
458
+
459
+ public func showFileDialogPreset(save: Bool, title: String, filter: String,
460
+ defaultName: String): String {
461
+ // GTK 轮未接预置文件名(自动化取证只在 Windows 侧做):如实降级为普通文件框
462
+ let _ = defaultName
463
+ var kind: Int32 = 0
464
+ if (save) {
465
+ kind = 1
466
+ }
467
+ var ok: Int32 = 0
468
+ unsafe {
469
+ ok = cj_bridge_show_dialog(this.handle, kind, LibC.mallocCString(title),
470
+ LibC.mallocCString(""), LibC.mallocCString(filter))
471
+ }
472
+ if (ok == 0) {
473
+ // 取消 / 宿主未就绪:上游按「空路径」处理,不再读结果位
474
+ return ""
475
+ }
476
+ return this.dialogResult
477
+ }
478
+
479
+ public func showFolderDialog(title: String): String {
480
+ var ok: Int32 = 0
481
+ unsafe {
482
+ // 选目录:kind=6(与文件类 / 消息类并列,见 bridge_core.h 的 dlg_req);
483
+ // message / filter 不参与,方向框也没有「默认文件名」的概念
484
+ ok = cj_bridge_show_dialog(this.handle, 6, LibC.mallocCString(title),
485
+ LibC.mallocCString(""), LibC.mallocCString(""))
486
+ }
487
+ if (ok == 0) {
488
+ // 取消 / 宿主未就绪:上游按「空路径」处理
489
+ return ""
490
+ }
491
+ return this.dialogResult
492
+ }
493
+
494
+ public func showMessageDialog(kind: String, title: String, message: String): Bool {
495
+ var ok: Int32 = 0
496
+ unsafe {
497
+ ok = cj_bridge_show_dialog(this.handle, dialogKindCode(kind), LibC.mallocCString(title),
498
+ LibC.mallocCString(message), LibC.mallocCString(""))
499
+ }
500
+ return ok != 0
501
+ }
502
+
503
+ public func clipboardWriteText(text: String): Bool {
504
+ var rc: Int32 = -1
505
+ unsafe {
506
+ rc = cj_bridge_clipboard_write_text(this.handle, LibC.mallocCString(text))
507
+ }
508
+ if (rc != 0) {
509
+ eprintln("[cj-tauri] clipboard write failed: code=${rc}")
510
+ }
511
+ return rc == 0
512
+ }
513
+
514
+ public func clipboardReadText(): Option<String> {
515
+ let cs = unsafe { cj_bridge_clipboard_read_text(this.handle) }
516
+ if (cs.isNull()) {
517
+ return None<String> // 空 / 非文本 / 平台失败,协议层不区分
518
+ }
519
+ let text = cs.toString()
520
+ unsafe {
521
+ cj_bridge_clipboard_free(cs) // 归还缓冲:malloc 家族分配,必须走桥的 free
522
+ }
523
+ return Some(text)
524
+ }
525
+
526
+ public func notify(title: String, body: String): Bool {
527
+ var rc: Int32 = -1
528
+ unsafe {
529
+ rc = cj_bridge_notification_notify(this.handle, LibC.mallocCString(title),
530
+ LibC.mallocCString(body))
531
+ }
532
+ if (rc != 0) {
533
+ // -4 = 本平台未实现:这是静态事实,stderr 一行说清,不算异常
534
+ eprintln("[cj-tauri] notification failed: code=${rc}")
535
+ }
536
+ return rc == 0
537
+ }
538
+
539
+ /**
540
+ * 创建宿主编(幂等):首次启动时分配,dispose() 后置空
541
+ */
542
+ private func ensureHandle(): Unit {
543
+ if (this.handle.isNull()) {
544
+ unsafe {
545
+ this.handle = cj_bridge_create()
546
+ }
547
+ }
548
+ }
549
+
550
+ /**
551
+ * 启动前的共同装配:宿主编、回调槽位、窗口配置、图标
552
+ */
553
+ private func prepareHost(): Unit {
554
+ this.ensureHandle()
555
+ HostGlobals.onMessage = this.onMessage
556
+ // 宿主级路由:「本句柄 → 本实例的三个回调」登记一条(表式,不覆盖别的窗口)
557
+ HostGlobals.registerHostRoute(this.handle, { json => this.shellHandler(json) },
558
+ { => this.onDestroy() },
559
+ { path => this.dialogResult = path })
560
+ // label → 句柄:附庸窗(setOwnerWindow)按 label 找属主窗,登记点与路由表同步
561
+ HostGlobals.registerHostHandle(this.windowLabel, this.handle)
562
+ var devTools: Int32 = 0
563
+ if (this.config.devTools) {
564
+ devTools = 1
565
+ }
566
+ unsafe {
567
+ cj_bridge_init(this.handle, hostOnMessage, hostOnDestroy)
568
+ // shell 事件回调(菜单点击等):与消息回调同一套路,多一个首参用来分辨窗口
569
+ cj_bridge_set_shell_callback(this.handle, shellOnEvent)
570
+ // 原生对话框的结果回调:与消息回调同一套路,多一个首参(结果只落发起这次对话框的窗口)
571
+ cj_bridge_set_dialog_callback(this.handle, dialogOnResult)
572
+ cj_bridge_set_window(this.handle, LibC.mallocCString(this.config.title),
573
+ Int32(this.config.width), Int32(this.config.height))
574
+ cj_bridge_set_devtools(this.handle, devTools)
575
+ // 窗口图标:空串表示系统默认图标
576
+ cj_bridge_set_icon(this.handle, LibC.mallocCString(this.config.iconPath))
577
+ // 预执行脚本(插件 jsShim 等):与 BRIDGE_JS 同一条 document-start 通道,按注册顺序排在桥之后
578
+ for (js in this.initScripts) {
579
+ cj_bridge_add_init_script(this.handle, LibC.mallocCString(js))
580
+ }
581
+ }
582
+ }
583
+
584
+ /**
585
+ * 加载页面(唯一入口,RFC §4.3):Inline/Url/DevServer 直接映射;
586
+ * Asset 经注入的 resolver 读盘,未注入时按 cwd 相对路径读(批次 1 的桌面行为)
587
+ */
588
+ public func load(source: PageSource): Unit {
589
+ match (resolvePage(source, this.assetResolver)) {
590
+ case StartHtml(html) => this.start(html)
591
+ case StartUrl(url) => this.startUrl(url)
592
+ }
593
+ }
594
+
595
+ /** 注入资产解析器(装配层在 load 前调用) */
596
+ public func setAssetResolver(resolver: Option<AssetResolver>): Unit {
597
+ this.assetResolver = resolver
598
+ }
599
+
600
+ public func start(html: String): Unit {
601
+ this.prepareHost()
602
+ unsafe {
603
+ cj_bridge_start(this.handle, LibC.mallocCString(html))
604
+ }
605
+ }
606
+
607
+ public func startUrl(url: String): Unit {
608
+ this.prepareHost()
609
+ unsafe {
610
+ cj_bridge_load_url(this.handle, LibC.mallocCString(url))
611
+ // 空串表示「页面由 cj_bridge_load_url 指定」(同一次启动里 URL 优先)
612
+ cj_bridge_start(this.handle, LibC.mallocCString(""))
613
+ }
614
+ }
615
+
616
+ public func reload(): Unit {
617
+ unsafe {
618
+ cj_bridge_reload(this.handle)
619
+ }
620
+ }
621
+
622
+ public func runJs(js: String): Unit {
623
+ unsafe {
624
+ cj_bridge_run_js(this.handle, LibC.mallocCString(js))
625
+ }
626
+ }
627
+
628
+ public func quit(): Unit {
629
+ unsafe {
630
+ cj_bridge_quit(this.handle)
631
+ }
632
+ }
633
+
634
+ public func shouldQuit(): Bool {
635
+ unsafe {
636
+ cj_bridge_should_quit(this.handle) != 0
637
+ }
638
+ }
639
+
640
+ /**
641
+ * 释放宿主编(幂等,未创建过则是空操作):事件循环退出后调用一次,之后句柄置空、本实例不可再启动
642
+ */
643
+ public func dispose(): Unit {
644
+ if (this.handle.isNull()) {
645
+ return
646
+ }
647
+ unsafe {
648
+ cj_bridge_destroy(this.handle)
649
+ }
650
+ this.handle = CPointer<Unit>()
651
+ }
652
+ }
653
+
654
+ // C 回调中转:C 桥线程 → 仓颉闭包(经全局槽位)
655
+ @When[os == "Linux" && env != "ohos"]
656
+ @C
657
+ func hostOnMessage(json: CString): Unit {
658
+ try {
659
+ HostGlobals.onMessage(json.toString())
660
+ } catch (e: Exception) {
661
+ eprintln("[cj-tauri] host callback error: ${e}")
662
+ }
663
+ }