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,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
+ }