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.
Files changed (158) hide show
  1. package/README.md +113 -99
  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 +632 -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 +526 -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 +3740 -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/prebuilt/linux-x64/cj-tauri +0 -0
  77. package/src/api_system.cj +67 -67
  78. package/src/app.cj +325 -969
  79. package/src/app_events.cj +166 -0
  80. package/src/app_host_ops.cj +287 -0
  81. package/src/app_run.cj +283 -0
  82. package/src/app_spec.cj +76 -76
  83. package/src/arkweb_menu.cj +216 -0
  84. package/src/arkweb_script.cj +142 -142
  85. package/src/capability.cj +409 -409
  86. package/src/clipboard_bridge.cj +178 -0
  87. package/src/dialog_spec.cj +79 -0
  88. package/src/dragdrop.cj +26 -26
  89. package/src/embedded_assets.cj +98 -98
  90. package/src/host.cj +752 -607
  91. package/src/host_arkweb.cj +924 -778
  92. package/src/host_webkit.cj +663 -570
  93. package/src/host_webview2.cj +663 -570
  94. package/src/hotkey.cj +66 -66
  95. package/src/ipc_hub.cj +337 -337
  96. package/src/ipc_message.cj +286 -286
  97. package/src/launch.cj +62 -62
  98. package/src/logging.cj +127 -127
  99. package/src/menu.cj +197 -197
  100. package/src/platform/platform_app_context.cj +77 -77
  101. package/src/platform/platform_assets.cj +33 -33
  102. package/src/platform/platform_discover.cj +176 -176
  103. package/src/platform/platform_env.cj +29 -29
  104. package/src/platform/platform_feature.cj +31 -31
  105. package/src/platform/platform_page_source.cj +87 -87
  106. package/src/platform/platform_target_info.cj +32 -32
  107. package/src/platform_host_provider.cj +87 -87
  108. package/src/plugin.cj +333 -333
  109. package/src/plugins/clipboard/plugin_clipboard.cj +152 -152
  110. package/src/plugins/dialog/plugin_dialog.cj +209 -209
  111. package/src/plugins/fs/plugin_fs.cj +105 -1
  112. package/src/plugins/http/http_backend.cj +131 -0
  113. package/src/plugins/http/http_desktop.cj +195 -0
  114. package/src/plugins/http/http_ohos.cj +204 -0
  115. package/src/plugins/http/plugin_http.cj +310 -459
  116. package/src/plugins/log/plugin_log.cj +105 -105
  117. package/src/plugins/menu/plugin_menu.cj +158 -158
  118. package/src/plugins/notification/plugin_notification.cj +154 -154
  119. package/src/plugins/serial/plugin_serial.cj +458 -454
  120. package/src/plugins/serial/serial_bridge.cj +194 -194
  121. package/src/plugins/serial/serial_manager_ohos.cj +92 -0
  122. package/src/plugins/serial/serial_ohos.cj +529 -0
  123. package/src/plugins/shell/plugin_shell.cj +258 -258
  124. package/src/plugins/sqlite/plugin_sqlite.cj +705 -710
  125. package/src/plugins/sqlite/sqlite_ffi.cj +206 -211
  126. package/src/plugins/ws/plugin_ws.cj +488 -470
  127. package/src/process_hidden.cj +174 -174
  128. package/src/scope.cj +391 -391
  129. package/src/shell_event.cj +69 -69
  130. package/src/tests/app_spec_test.cj +126 -127
  131. package/src/tests/capability_test.cj +243 -243
  132. package/src/tests/dialog_spec_test.cj +73 -0
  133. package/src/tests/fake_host.cj +201 -201
  134. package/src/tests/ipc_hub_test.cj +487 -487
  135. package/src/tests/logging_test.cj +62 -62
  136. package/src/tests/platform_contract_test.cj +251 -251
  137. package/src/tests/plugin_clipboard_test.cj +222 -222
  138. package/src/tests/plugin_dialog_test.cj +237 -237
  139. package/src/tests/plugin_fs_test.cj +207 -0
  140. package/src/tests/plugin_http_test.cj +124 -124
  141. package/src/tests/plugin_notification_test.cj +205 -205
  142. package/src/tests/plugin_serial_test.cj +48 -0
  143. package/src/tests/plugin_test.cj +605 -585
  144. package/src/tests/plugin_ws_test.cj +40 -40
  145. package/src/tests/process_hidden_test.cj +129 -129
  146. package/src/tests/scope_test.cj +350 -350
  147. package/src/tests/system_host_test.cj +99 -81
  148. package/src/tests/urlutil_test.cj +53 -53
  149. package/src/tests/version_test.cj +75 -75
  150. package/src/tests/window_registry_test.cj +123 -123
  151. package/src/tray.cj +90 -90
  152. package/src/urlutil.cj +159 -159
  153. package/src/version.cj +44 -44
  154. package/src/window.cj +51 -51
  155. package/src/window_registry.cj +116 -116
  156. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  157. package/src/plugins/serial/serial_ohos_stub.cj +0 -61
  158. package/src/plugins/sqlite/sqlite_ohos_stub.cj +0 -91
@@ -1,123 +1,123 @@
1
- package cjTauri.tests
2
-
3
- import cjTauri.*
4
- import std.collection.*
5
- import std.unittest.*
6
- import std.unittest.testmacro.*
7
-
8
- /*
9
- * 窗口注册表单测:纯逻辑,不创建窗口,可在无 GUI 环境跑。
10
- * 两条对外契约借用例钉住:① label 缺省值 "main"(现有示例与探针写死它);
11
- * ② labels() 按登记顺序(广播据此保持确定顺序,HashMap 的迭代顺序不保证)。
12
- */
13
-
14
- /** 断言某段调用抛异常(此处要断的是「拒绝」这个行为本身,@Expect 断言不了) */
15
- func thrownBy(body: () -> Unit): Bool {
16
- try {
17
- body()
18
- return false
19
- } catch (e: Exception) {
20
- return true
21
- }
22
- }
23
-
24
- func rec(label: String): WindowRecord {
25
- return WindowRecord(label, FakeHost(label), WindowConfig())
26
- }
27
-
28
- @Test
29
- func testRegistryAddAndGet(): Unit {
30
- let reg = WindowRegistry()
31
- reg.add(rec("main"))
32
- @Expect(reg.size(), 1)
33
- @Expect(reg.get("main").isSome(), true)
34
- @Expect(reg.get("main").getOrThrow().label, "main")
35
- }
36
-
37
- @Test
38
- func testRegistryRejectsDuplicateLabel(): Unit {
39
- let reg = WindowRegistry()
40
- reg.add(rec("main"))
41
- // 覆盖会让「一个 label 挂着两个宿主」静默成立,必须抛
42
- @Expect(thrownBy({ => reg.add(rec("main")) }), true)
43
- @Expect(reg.size(), 1)
44
- }
45
-
46
- @Test
47
- func testRegistryRejectsEmptyLabel(): Unit {
48
- let reg = WindowRegistry()
49
- @Expect(thrownBy({ => reg.add(rec("")) }), true)
50
- @Expect(reg.size(), 0)
51
- }
52
-
53
- @Test
54
- func testRegistryMissingLabelIsNone(): Unit {
55
- let reg = WindowRegistry()
56
- @Expect(reg.get("settings").isNone(), true)
57
- }
58
-
59
- @Test
60
- func testRegistryLabelsKeepInsertionOrder(): Unit {
61
- let reg = WindowRegistry()
62
- reg.add(rec("main"))
63
- reg.add(rec("settings"))
64
- reg.add(rec("log"))
65
- let ls = reg.labels()
66
- @Expect(ls.size, 3)
67
- @Expect(ls[0], "main")
68
- @Expect(ls[1], "settings")
69
- @Expect(ls[2], "log")
70
- }
71
-
72
- @Test
73
- func testRegistryRemoveIsIdempotent(): Unit {
74
- let reg = WindowRegistry()
75
- reg.add(rec("main"))
76
- reg.add(rec("settings"))
77
- reg.remove("main")
78
- @Expect(reg.size(), 1)
79
- @Expect(reg.get("main").isNone(), true)
80
- // 关窗路径可能重复触发:删不存在的 label 不该抛
81
- reg.remove("main")
82
- @Expect(reg.size(), 1)
83
- let ls = reg.labels()
84
- @Expect(ls.size, 1)
85
- @Expect(ls[0], "settings")
86
- }
87
-
88
- @Test
89
- func testRecordKeepsConfigSnapshot(): Unit {
90
- let r = WindowRecord("main", FakeHost("main"), WindowConfig("编辑器", 1024, 768))
91
- @Expect(r.config.title, "编辑器")
92
- @Expect(r.config.width, 1024)
93
- @Expect(r.config.height, 768)
94
- }
95
-
96
- @Test
97
- func testTargetsBroadcastKeepsOrder(): Unit {
98
- let reg = WindowRegistry()
99
- reg.add(rec("main"))
100
- reg.add(rec("settings"))
101
- let ts = reg.targets("")
102
- @Expect(ts.size, 2)
103
- @Expect(ts[0].label, "main")
104
- @Expect(ts[1].label, "settings")
105
- }
106
-
107
- @Test
108
- func testTargetsSingleWindow(): Unit {
109
- let reg = WindowRegistry()
110
- reg.add(rec("main"))
111
- reg.add(rec("settings"))
112
- let ts = reg.targets("settings")
113
- @Expect(ts.size, 1)
114
- @Expect(ts[0].label, "settings")
115
- }
116
-
117
- @Test
118
- func testTargetsUnknownLabelIsEmpty(): Unit {
119
- let reg = WindowRegistry()
120
- reg.add(rec("main"))
121
- // 未注册的 label 不回落成广播:写错 label 就该听不到,由调用方打日志
122
- @Expect(reg.targets("nope").size, 0)
123
- }
1
+ package cjTauri.tests
2
+
3
+ import cjTauri.*
4
+ import std.collection.*
5
+ import std.unittest.*
6
+ import std.unittest.testmacro.*
7
+
8
+ /*
9
+ * 窗口注册表单测:纯逻辑,不创建窗口,可在无 GUI 环境跑。
10
+ * 两条对外契约借用例钉住:① label 缺省值 "main"(现有示例与探针写死它);
11
+ * ② labels() 按登记顺序(广播据此保持确定顺序,HashMap 的迭代顺序不保证)。
12
+ */
13
+
14
+ /** 断言某段调用抛异常(此处要断的是「拒绝」这个行为本身,@Expect 断言不了) */
15
+ func thrownBy(body: () -> Unit): Bool {
16
+ try {
17
+ body()
18
+ return false
19
+ } catch (e: Exception) {
20
+ return true
21
+ }
22
+ }
23
+
24
+ func rec(label: String): WindowRecord {
25
+ return WindowRecord(label, FakeHost(label), WindowConfig())
26
+ }
27
+
28
+ @Test
29
+ func testRegistryAddAndGet(): Unit {
30
+ let reg = WindowRegistry()
31
+ reg.add(rec("main"))
32
+ @Expect(reg.size(), 1)
33
+ @Expect(reg.get("main").isSome(), true)
34
+ @Expect(reg.get("main").getOrThrow().label, "main")
35
+ }
36
+
37
+ @Test
38
+ func testRegistryRejectsDuplicateLabel(): Unit {
39
+ let reg = WindowRegistry()
40
+ reg.add(rec("main"))
41
+ // 覆盖会让「一个 label 挂着两个宿主」静默成立,必须抛
42
+ @Expect(thrownBy({ => reg.add(rec("main")) }), true)
43
+ @Expect(reg.size(), 1)
44
+ }
45
+
46
+ @Test
47
+ func testRegistryRejectsEmptyLabel(): Unit {
48
+ let reg = WindowRegistry()
49
+ @Expect(thrownBy({ => reg.add(rec("")) }), true)
50
+ @Expect(reg.size(), 0)
51
+ }
52
+
53
+ @Test
54
+ func testRegistryMissingLabelIsNone(): Unit {
55
+ let reg = WindowRegistry()
56
+ @Expect(reg.get("settings").isNone(), true)
57
+ }
58
+
59
+ @Test
60
+ func testRegistryLabelsKeepInsertionOrder(): Unit {
61
+ let reg = WindowRegistry()
62
+ reg.add(rec("main"))
63
+ reg.add(rec("settings"))
64
+ reg.add(rec("log"))
65
+ let ls = reg.labels()
66
+ @Expect(ls.size, 3)
67
+ @Expect(ls[0], "main")
68
+ @Expect(ls[1], "settings")
69
+ @Expect(ls[2], "log")
70
+ }
71
+
72
+ @Test
73
+ func testRegistryRemoveIsIdempotent(): Unit {
74
+ let reg = WindowRegistry()
75
+ reg.add(rec("main"))
76
+ reg.add(rec("settings"))
77
+ reg.remove("main")
78
+ @Expect(reg.size(), 1)
79
+ @Expect(reg.get("main").isNone(), true)
80
+ // 关窗路径可能重复触发:删不存在的 label 不该抛
81
+ reg.remove("main")
82
+ @Expect(reg.size(), 1)
83
+ let ls = reg.labels()
84
+ @Expect(ls.size, 1)
85
+ @Expect(ls[0], "settings")
86
+ }
87
+
88
+ @Test
89
+ func testRecordKeepsConfigSnapshot(): Unit {
90
+ let r = WindowRecord("main", FakeHost("main"), WindowConfig("编辑器", 1024, 768))
91
+ @Expect(r.config.title, "编辑器")
92
+ @Expect(r.config.width, 1024)
93
+ @Expect(r.config.height, 768)
94
+ }
95
+
96
+ @Test
97
+ func testTargetsBroadcastKeepsOrder(): Unit {
98
+ let reg = WindowRegistry()
99
+ reg.add(rec("main"))
100
+ reg.add(rec("settings"))
101
+ let ts = reg.targets("")
102
+ @Expect(ts.size, 2)
103
+ @Expect(ts[0].label, "main")
104
+ @Expect(ts[1].label, "settings")
105
+ }
106
+
107
+ @Test
108
+ func testTargetsSingleWindow(): Unit {
109
+ let reg = WindowRegistry()
110
+ reg.add(rec("main"))
111
+ reg.add(rec("settings"))
112
+ let ts = reg.targets("settings")
113
+ @Expect(ts.size, 1)
114
+ @Expect(ts[0].label, "settings")
115
+ }
116
+
117
+ @Test
118
+ func testTargetsUnknownLabelIsEmpty(): Unit {
119
+ let reg = WindowRegistry()
120
+ reg.add(rec("main"))
121
+ // 未注册的 label 不回落成广播:写错 label 就该听不到,由调用方打日志
122
+ @Expect(reg.targets("nope").size, 0)
123
+ }
package/src/tray.cj CHANGED
@@ -1,90 +1,90 @@
1
- package cjTauri
2
-
3
- import std.collection.*
4
-
5
- /**
6
- * 托盘的**纯仓颉模型**(RFC-002 §5.1 / §7.C):只描述意图,不含任何平台概念。
7
- *
8
- * 分层与菜单同构:模型(本文件)→ `WebViewHost.setTray()` → C 桥 `cj_bridge_set_tray` →
9
- * 平台翻译层(Linux = libayatana-appindicator3,可选编译;Windows 桩如实报未实现)。
10
- *
11
- * 托盘是**应用级**资源(一个进程一个图标):重复 setTray = 整体重建(图标 / 提示 / 菜单一起换);
12
- * 空线路 = 移除托盘。菜单项复用 `MenuItem`(v1 只收 Normal / Checkbox / Separator,
13
- * Submenu 整条跳过并打 stderr——托盘菜单平铺,别给子菜单)。
14
- *
15
- * 线路格式(与 `native/core/bridge_core.h` 的 `pending_tray` 注释逐字对应):
16
- *
17
- * icon\t<路径>\n (空路径 = 平台用主题默认图标兜底)
18
- * tooltip\t<文本>\n
19
- * <kind>\t<id>\t<label>\t<flags>\t<accel>\n (与菜单线路同格式,平铺一层)
20
- *
21
- * 菜单项被点以 `{"kind":"tray","id":"…"}` 走 shell 通道回投(前端事件 `tray:click`,
22
- * 需在能力清单 `events` 声明)。图标 / 提示文本来自应用作者,不做转义;
23
- * 字段含制表符 / 换行的头部值整体丢弃为空(wire 的结构字符,与菜单同一铁律)。
24
- */
25
- public class TrayModel {
26
- /** 图标路径(png/ico 绝对路径);空串 = 平台用主题默认图标兜底 */
27
- public let icon: String
28
-
29
- /** 悬停提示文本;空串 = 不设 */
30
- public let tooltip: String
31
-
32
- /** 托盘右键菜单(平铺一层;Submenu 项会被 toWire 丢弃并打 stderr) */
33
- public let items: ArrayList<MenuItem>
34
-
35
- public init(icon: String, tooltip: String, items: ArrayList<MenuItem>) {
36
- this.icon = icon
37
- this.tooltip = tooltip
38
- this.items = items
39
- }
40
-
41
- /** 无菜单托盘(纯图标 + 提示) */
42
- public init(icon: String, tooltip: String) {
43
- this.icon = icon
44
- this.tooltip = tooltip
45
- this.items = ArrayList<MenuItem>()
46
- }
47
-
48
- /** 线路文本(三段:icon 行 / tooltip 行 / 菜单行走 MenuModel.emit 同一遍发射器) */
49
- public func toWire(): String {
50
- // 托盘菜单平铺:Submenu 整条出局(枚举不能写 ==,AGENTS §4,走 match)
51
- let flat = ArrayList<MenuItem>()
52
- for (it in this.items) {
53
- if (TrayModel.isSubmenu(it.kind)) {
54
- eprintln("[cj-tauri] tray: submenu item dropped (v1 tray menu is flat): ${it.label}")
55
- continue
56
- }
57
- flat.add(it)
58
- }
59
- // 头部两行的结构字符会让整份线路解析错位:宁可丢提示也不装错
60
- var icon = this.icon
61
- var tooltip = this.tooltip
62
- if (TrayModel.hasStructChar(icon) || TrayModel.hasStructChar(tooltip)) {
63
- eprintln("[cj-tauri] tray: icon/tooltip contains tab or newline - dropped to empty")
64
- icon = ""
65
- tooltip = ""
66
- }
67
- var out = "icon\t" + icon + "\n"
68
- out += "tooltip\t" + tooltip + "\n"
69
- out += MenuModel.emit(flat, 0)
70
- return out
71
- }
72
-
73
- /** 枚举判别只能走 match(`==` 编译不过,见 MenuModel.isSubmenu 的实测注释) */
74
- private static func isSubmenu(kind: MenuItemKind): Bool {
75
- match (kind) {
76
- case MenuItemKind.Submenu => true
77
- case _ => false
78
- }
79
- }
80
-
81
- /** 结构字符(tab/LF/CR)检查——wire 的分隔符就是它们 */
82
- private static func hasStructChar(s: String): Bool {
83
- for (b in s) {
84
- if (b == 9u8 || b == 10u8 || b == 13u8) {
85
- return true
86
- }
87
- }
88
- return false
89
- }
90
- }
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+
5
+ /**
6
+ * 托盘的**纯仓颉模型**(RFC-002 §5.1 / §7.C):只描述意图,不含任何平台概念。
7
+ *
8
+ * 分层与菜单同构:模型(本文件)→ `WebViewHost.setTray()` → C 桥 `cj_bridge_set_tray` →
9
+ * 平台翻译层(Linux = libayatana-appindicator3,可选编译;Windows 桩如实报未实现)。
10
+ *
11
+ * 托盘是**应用级**资源(一个进程一个图标):重复 setTray = 整体重建(图标 / 提示 / 菜单一起换);
12
+ * 空线路 = 移除托盘。菜单项复用 `MenuItem`(v1 只收 Normal / Checkbox / Separator,
13
+ * Submenu 整条跳过并打 stderr——托盘菜单平铺,别给子菜单)。
14
+ *
15
+ * 线路格式(与 `native/core/bridge_core.h` 的 `pending_tray` 注释逐字对应):
16
+ *
17
+ * icon\t<路径>\n (空路径 = 平台用主题默认图标兜底)
18
+ * tooltip\t<文本>\n
19
+ * <kind>\t<id>\t<label>\t<flags>\t<accel>\n (与菜单线路同格式,平铺一层)
20
+ *
21
+ * 菜单项被点以 `{"kind":"tray","id":"…"}` 走 shell 通道回投(前端事件 `tray:click`,
22
+ * 需在能力清单 `events` 声明)。图标 / 提示文本来自应用作者,不做转义;
23
+ * 字段含制表符 / 换行的头部值整体丢弃为空(wire 的结构字符,与菜单同一铁律)。
24
+ */
25
+ public class TrayModel {
26
+ /** 图标路径(png/ico 绝对路径);空串 = 平台用主题默认图标兜底 */
27
+ public let icon: String
28
+
29
+ /** 悬停提示文本;空串 = 不设 */
30
+ public let tooltip: String
31
+
32
+ /** 托盘右键菜单(平铺一层;Submenu 项会被 toWire 丢弃并打 stderr) */
33
+ public let items: ArrayList<MenuItem>
34
+
35
+ public init(icon: String, tooltip: String, items: ArrayList<MenuItem>) {
36
+ this.icon = icon
37
+ this.tooltip = tooltip
38
+ this.items = items
39
+ }
40
+
41
+ /** 无菜单托盘(纯图标 + 提示) */
42
+ public init(icon: String, tooltip: String) {
43
+ this.icon = icon
44
+ this.tooltip = tooltip
45
+ this.items = ArrayList<MenuItem>()
46
+ }
47
+
48
+ /** 线路文本(三段:icon 行 / tooltip 行 / 菜单行走 MenuModel.emit 同一遍发射器) */
49
+ public func toWire(): String {
50
+ // 托盘菜单平铺:Submenu 整条出局(枚举不能写 ==,AGENTS §4,走 match)
51
+ let flat = ArrayList<MenuItem>()
52
+ for (it in this.items) {
53
+ if (TrayModel.isSubmenu(it.kind)) {
54
+ eprintln("[cj-tauri] tray: submenu item dropped (v1 tray menu is flat): ${it.label}")
55
+ continue
56
+ }
57
+ flat.add(it)
58
+ }
59
+ // 头部两行的结构字符会让整份线路解析错位:宁可丢提示也不装错
60
+ var icon = this.icon
61
+ var tooltip = this.tooltip
62
+ if (TrayModel.hasStructChar(icon) || TrayModel.hasStructChar(tooltip)) {
63
+ eprintln("[cj-tauri] tray: icon/tooltip contains tab or newline - dropped to empty")
64
+ icon = ""
65
+ tooltip = ""
66
+ }
67
+ var out = "icon\t" + icon + "\n"
68
+ out += "tooltip\t" + tooltip + "\n"
69
+ out += MenuModel.emit(flat, 0)
70
+ return out
71
+ }
72
+
73
+ /** 枚举判别只能走 match(`==` 编译不过,见 MenuModel.isSubmenu 的实测注释) */
74
+ private static func isSubmenu(kind: MenuItemKind): Bool {
75
+ match (kind) {
76
+ case MenuItemKind.Submenu => true
77
+ case _ => false
78
+ }
79
+ }
80
+
81
+ /** 结构字符(tab/LF/CR)检查——wire 的分隔符就是它们 */
82
+ private static func hasStructChar(s: String): Bool {
83
+ for (b in s) {
84
+ if (b == 9u8 || b == 10u8 || b == 13u8) {
85
+ return true
86
+ }
87
+ }
88
+ return false
89
+ }
90
+ }