cj-tauri 0.8.0 → 1.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 +3 -3
- package/cjpm.toml +52 -25
- package/cli/cjpm.toml +1 -1
- package/cli/src/doctor.cj +304 -0
- package/cli/src/main.cj +33 -7
- package/cli/src/project.cj +59 -1
- package/cli/src/resolve.cj +348 -304
- package/cli/src/scaffold.cj +139 -22
- package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/README.md +79 -0
- package/cli/templates/ohos-pc/build-hap.sh +190 -0
- package/cli/templates/ohos-pc/build-profile.json5 +46 -0
- package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
- package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
- package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
- package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
- package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
- package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
- package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
- package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
- package/cli/templates/ohos-pc/oh-package.json5 +10 -0
- package/native/build_linux.sh +30 -2
- package/native/build_win.bat +62 -61
- package/native/core/bridge_core.c +1626 -0
- package/native/{bridge_core.h → core/bridge_core.h} +246 -6
- package/native/platforms/harmonyos/README.md +106 -0
- package/native/platforms/linux/bridge_linux.c +2485 -0
- package/native/platforms/windows/bridge_win.c +3248 -0
- package/native/tests/test_bridge_core.c +528 -1
- package/package.json +12 -3
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/api_system.cj +10 -2
- package/src/app.cj +248 -85
- package/src/arkweb_proxy.cj +56 -0
- package/src/arkweb_script.cj +154 -0
- package/src/capability.cj +140 -0
- package/src/clipboard_bridge.cj +55 -0
- package/src/dragdrop.cj +26 -0
- package/src/embedded_assets.cj +98 -0
- package/src/hexutil.cj +53 -0
- package/src/host.cj +197 -26
- package/src/host_arkweb.cj +740 -0
- package/src/host_factory.cj +48 -0
- package/src/host_webkit.cj +202 -26
- package/src/host_webview2.cj +176 -3
- package/src/hotkey.cj +66 -0
- package/src/ipc_hub.cj +49 -0
- package/src/ipc_message.cj +2 -1
- package/src/jsutil.cj +33 -0
- package/src/log_bridge.cj +107 -0
- package/src/logging.cj +127 -0
- package/src/menu.cj +197 -196
- package/src/plugin.cj +40 -0
- package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
- package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
- package/src/plugins/fs/plugin_fs.cj +194 -0
- package/src/plugins/http/plugin_http.cj +459 -0
- package/src/plugins/log/plugin_log.cj +105 -0
- package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
- package/src/plugins/mqtt/mqtt_codec.cj +661 -0
- package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
- package/src/plugins/notification/plugin_notification.cj +146 -0
- package/src/plugins/plugins.cj +5 -0
- package/src/plugins/redis/plugin_redis.cj +2409 -0
- package/src/plugins/redis/redis_codec.cj +556 -0
- package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
- package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
- package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
- package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
- package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
- package/src/plugins/store/plugin_store.cj +287 -0
- package/src/plugins/ws/plugin_ws.cj +470 -0
- package/src/process_hidden.cj +174 -0
- package/src/scope.cj +391 -0
- package/src/shell_event.cj +69 -68
- package/src/tests/capability_test.cj +53 -0
- package/src/tests/fake_host.cj +85 -20
- package/src/tests/ipc_hub_test.cj +13 -0
- package/src/tests/jsutil_test.cj +62 -0
- package/src/tests/logging_test.cj +62 -0
- package/src/tests/mqtt_codec_test.cj +405 -0
- package/src/tests/plugin_clipboard_test.cj +222 -0
- package/src/tests/plugin_dialog_test.cj +98 -4
- package/src/tests/plugin_http_test.cj +124 -0
- package/src/tests/plugin_menu_test.cj +1 -0
- package/src/tests/plugin_notification_test.cj +205 -0
- package/src/tests/plugin_serial_test.cj +269 -267
- package/src/tests/plugin_shell_test.cj +189 -188
- package/src/tests/plugin_sqlite_test.cj +230 -0
- package/src/tests/plugin_store_test.cj +260 -0
- package/src/tests/plugin_test.cj +542 -524
- package/src/tests/plugin_ws_test.cj +40 -0
- package/src/tests/process_hidden_test.cj +129 -0
- package/src/tests/redis_codec_test.cj +396 -0
- package/src/tests/scope_test.cj +350 -0
- package/src/tests/shell_event_test.cj +0 -1
- package/src/tests/system_host_test.cj +81 -79
- package/src/tests/urlutil_test.cj +53 -0
- package/src/tests/version_test.cj +75 -65
- package/src/tray.cj +90 -0
- package/src/urlutil.cj +159 -0
- package/src/version.cj +23 -2
- package/src/window.cj +51 -42
- package/native/bridge_core.c +0 -797
- package/native/bridge_linux.c +0 -1268
- package/native/bridge_win.c +0 -1633
- package/src/plugin_fs.cj +0 -118
- /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
package/src/menu.cj
CHANGED
|
@@ -1,196 +1,197 @@
|
|
|
1
|
-
package cjTauri
|
|
2
|
-
|
|
3
|
-
import std.collection.*
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* 窗口菜单栏的**纯仓颉模型**(RFC-002 §5.1):只描述意图,不含任何平台概念。
|
|
7
|
-
*
|
|
8
|
-
* 分层:模型(本文件)→ `WebViewHost.setMenu()`(src/host.cj)→ C 桥 `cj_bridge_set_menu` →
|
|
9
|
-
* 平台翻译层(native/bridge_win.c 的 CreateMenu 族 / bridge_linux.c 的 GtkMenuBar)。
|
|
10
|
-
* 菜单文本来自**应用作者**(不是页面可控的不可信输入),所以这里不做转义——
|
|
11
|
-
* 非法字段让那一项出局并出声,而不是写一套转义/反转义。
|
|
12
|
-
*/
|
|
13
|
-
public enum MenuItemKind {
|
|
14
|
-
| Normal // 普通项:点击回调自己的 id
|
|
15
|
-
| Checkbox // 可勾选项:回调里带 checked
|
|
16
|
-
| Separator // 分隔线:无 id、不可点
|
|
17
|
-
| Submenu // 子菜单:自身不可点,只承载 children
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
public class MenuItem {
|
|
21
|
-
public let id: String
|
|
22
|
-
public let label: String
|
|
23
|
-
public let kind: MenuItemKind
|
|
24
|
-
/** 快捷键**提示**文本(如 "Ctrl+S")。v1 只显示,不抢页面按键 */
|
|
25
|
-
public let accelerator: String
|
|
26
|
-
public var enabled: Bool
|
|
27
|
-
public var checked: Bool
|
|
28
|
-
/** 仅 Submenu 用;其它 kind 恒为空表 */
|
|
29
|
-
public let children: ArrayList<MenuItem>
|
|
30
|
-
|
|
31
|
-
public init(id: String, label: String, kind: MenuItemKind, accelerator: String,
|
|
32
|
-
enabled: Bool, checked: Bool, children: ArrayList<MenuItem>) {
|
|
33
|
-
this.id = id
|
|
34
|
-
this.label = label
|
|
35
|
-
this.kind = kind
|
|
36
|
-
this.accelerator = accelerator
|
|
37
|
-
this.enabled = enabled
|
|
38
|
-
this.checked = checked
|
|
39
|
-
this.children = children
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
/** 普通菜单项(最常用) */
|
|
43
|
-
public static func normal(id: String, label: String): MenuItem {
|
|
44
|
-
return MenuItem(id, label, MenuItemKind.Normal, "", true, false, ArrayList<MenuItem>())
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** 带快捷键提示的普通项 */
|
|
48
|
-
public static func normalWithAccel(id: String, label: String, accelerator: String): MenuItem {
|
|
49
|
-
return MenuItem(id, label, MenuItemKind.Normal, accelerator, true, false, ArrayList<MenuItem>())
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** 勾选项(初始 checked) */
|
|
53
|
-
public static func checkbox(id: String, label: String, checked: Bool): MenuItem {
|
|
54
|
-
return MenuItem(id, label, MenuItemKind.Checkbox, "", true, checked, ArrayList<MenuItem>())
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** 子菜单:id 留空(自身不回调),动作交给 children */
|
|
58
|
-
public static func submenu(label: String, children: ArrayList<MenuItem>): MenuItem {
|
|
59
|
-
return MenuItem("", label, MenuItemKind.Submenu, "", true, false, children)
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/** 分隔线 */
|
|
63
|
-
public static func separator(): MenuItem {
|
|
64
|
-
return MenuItem("", "", MenuItemKind.Separator, "", true, false, ArrayList<MenuItem>())
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* 菜单模型:`items` 是菜单栏的顶层,子菜单靠 `MenuItem.children` 递归。
|
|
70
|
-
*
|
|
71
|
-
* **不可变**(items 只读):运行期改状态走 `WebViewHost.setMenuItemState(id, …)`,不在这里改——
|
|
72
|
-
* 状态的真身在平台对象上(Windows 的 HMENU、GTK 的 GtkCheckMenuItem),只改模型平台不会自己变。
|
|
73
|
-
*/
|
|
74
|
-
public class MenuModel {
|
|
75
|
-
public let items: ArrayList<MenuItem>
|
|
76
|
-
|
|
77
|
-
public init(items: ArrayList<MenuItem>) {
|
|
78
|
-
this.items = items
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* 按 id 找菜单项(本层优先,再下钻子菜单);找不到返回 None
|
|
83
|
-
*
|
|
84
|
-
* 为什么要它:运行期改状态按 id 定位(`setMenuItemState`),而**应用层的模型状态**必须
|
|
85
|
-
* 能反查出「这个 id 现在是什么状态」——平台侧一次写两位(enabled + checked,见
|
|
86
|
-
* `WebViewHost.setMenuItemState`),拿不到另一维就会把用户刚点出来的勾选抹掉。
|
|
87
|
-
*/
|
|
88
|
-
public func findById(id: String): Option<MenuItem> {
|
|
89
|
-
return MenuModel.lookup(this.items, id)
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
private static func lookup(items: ArrayList<MenuItem>, id: String): Option<MenuItem> {
|
|
93
|
-
for (item in items) {
|
|
94
|
-
if (item.id == id) {
|
|
95
|
-
return Some(item)
|
|
96
|
-
}
|
|
97
|
-
match (MenuModel.lookup(item.children, id)) {
|
|
98
|
-
case Some(found) => return Some(found)
|
|
99
|
-
case None => ()
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
return None
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* 线路格式(RFC-002 §5.4):一行一项,字段用制表符分隔,**行首制表符数 = 层级**。
|
|
107
|
-
*
|
|
108
|
-
* <tabs><kind>\t<id>\t<label>\t<flags>\t<accel>\n
|
|
109
|
-
*
|
|
110
|
-
* kind:`n` 普通 / `c` 勾选 / `s` 分隔线 / `m` 子菜单;
|
|
111
|
-
* flags:十进制,bit0 = 可用、bit1 = 勾选(`3` = 又可用又勾选);
|
|
112
|
-
* 分隔线与子菜单也走满 5 个字段(id / label / accel 为空),C 侧因此不必为它们开特例。
|
|
113
|
-
*
|
|
114
|
-
* 为什么 kind 放最前:C 侧先分派再取值,日志里一眼能看出这行是什么。
|
|
115
|
-
* 为什么不是 JSON:C 侧(bridge_core.c)是纯 C、不引第三方,行式文本解析几十行就够,
|
|
116
|
-
* 且坏了能直接 `fprintf` 到 stderr(诊断走 stderr 是项目铁律)。JSON 留在仓颉侧给 CLI / 清单用。
|
|
117
|
-
* 为什么不做转义:字段里出现制表符 / 换行会破坏「一行一项」的结构——v1 的口径是
|
|
118
|
-
* **丢弃该项 + stderr 逐项提示**(见 emit),而不是引入转义/反转义这一对必须同时正确的函数。
|
|
119
|
-
*/
|
|
120
|
-
public func toWire(): String {
|
|
121
|
-
return MenuModel.emit(this.items, 0)
|
|
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
|
-
case
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
var
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
case MenuItemKind.
|
|
180
|
-
case MenuItemKind.
|
|
181
|
-
case MenuItemKind.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
}
|
|
1
|
+
package cjTauri
|
|
2
|
+
|
|
3
|
+
import std.collection.*
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 窗口菜单栏的**纯仓颉模型**(RFC-002 §5.1):只描述意图,不含任何平台概念。
|
|
7
|
+
*
|
|
8
|
+
* 分层:模型(本文件)→ `WebViewHost.setMenu()`(src/host.cj)→ C 桥 `cj_bridge_set_menu` →
|
|
9
|
+
* 平台翻译层(native/bridge_win.c 的 CreateMenu 族 / bridge_linux.c 的 GtkMenuBar)。
|
|
10
|
+
* 菜单文本来自**应用作者**(不是页面可控的不可信输入),所以这里不做转义——
|
|
11
|
+
* 非法字段让那一项出局并出声,而不是写一套转义/反转义。
|
|
12
|
+
*/
|
|
13
|
+
public enum MenuItemKind {
|
|
14
|
+
| Normal // 普通项:点击回调自己的 id
|
|
15
|
+
| Checkbox // 可勾选项:回调里带 checked
|
|
16
|
+
| Separator // 分隔线:无 id、不可点
|
|
17
|
+
| Submenu // 子菜单:自身不可点,只承载 children
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
public class MenuItem {
|
|
21
|
+
public let id: String
|
|
22
|
+
public let label: String
|
|
23
|
+
public let kind: MenuItemKind
|
|
24
|
+
/** 快捷键**提示**文本(如 "Ctrl+S")。v1 只显示,不抢页面按键 */
|
|
25
|
+
public let accelerator: String
|
|
26
|
+
public var enabled: Bool
|
|
27
|
+
public var checked: Bool
|
|
28
|
+
/** 仅 Submenu 用;其它 kind 恒为空表 */
|
|
29
|
+
public let children: ArrayList<MenuItem>
|
|
30
|
+
|
|
31
|
+
public init(id: String, label: String, kind: MenuItemKind, accelerator: String,
|
|
32
|
+
enabled: Bool, checked: Bool, children: ArrayList<MenuItem>) {
|
|
33
|
+
this.id = id
|
|
34
|
+
this.label = label
|
|
35
|
+
this.kind = kind
|
|
36
|
+
this.accelerator = accelerator
|
|
37
|
+
this.enabled = enabled
|
|
38
|
+
this.checked = checked
|
|
39
|
+
this.children = children
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** 普通菜单项(最常用) */
|
|
43
|
+
public static func normal(id: String, label: String): MenuItem {
|
|
44
|
+
return MenuItem(id, label, MenuItemKind.Normal, "", true, false, ArrayList<MenuItem>())
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** 带快捷键提示的普通项 */
|
|
48
|
+
public static func normalWithAccel(id: String, label: String, accelerator: String): MenuItem {
|
|
49
|
+
return MenuItem(id, label, MenuItemKind.Normal, accelerator, true, false, ArrayList<MenuItem>())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** 勾选项(初始 checked) */
|
|
53
|
+
public static func checkbox(id: String, label: String, checked: Bool): MenuItem {
|
|
54
|
+
return MenuItem(id, label, MenuItemKind.Checkbox, "", true, checked, ArrayList<MenuItem>())
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** 子菜单:id 留空(自身不回调),动作交给 children */
|
|
58
|
+
public static func submenu(label: String, children: ArrayList<MenuItem>): MenuItem {
|
|
59
|
+
return MenuItem("", label, MenuItemKind.Submenu, "", true, false, children)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 分隔线 */
|
|
63
|
+
public static func separator(): MenuItem {
|
|
64
|
+
return MenuItem("", "", MenuItemKind.Separator, "", true, false, ArrayList<MenuItem>())
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 菜单模型:`items` 是菜单栏的顶层,子菜单靠 `MenuItem.children` 递归。
|
|
70
|
+
*
|
|
71
|
+
* **不可变**(items 只读):运行期改状态走 `WebViewHost.setMenuItemState(id, …)`,不在这里改——
|
|
72
|
+
* 状态的真身在平台对象上(Windows 的 HMENU、GTK 的 GtkCheckMenuItem),只改模型平台不会自己变。
|
|
73
|
+
*/
|
|
74
|
+
public class MenuModel {
|
|
75
|
+
public let items: ArrayList<MenuItem>
|
|
76
|
+
|
|
77
|
+
public init(items: ArrayList<MenuItem>) {
|
|
78
|
+
this.items = items
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* 按 id 找菜单项(本层优先,再下钻子菜单);找不到返回 None
|
|
83
|
+
*
|
|
84
|
+
* 为什么要它:运行期改状态按 id 定位(`setMenuItemState`),而**应用层的模型状态**必须
|
|
85
|
+
* 能反查出「这个 id 现在是什么状态」——平台侧一次写两位(enabled + checked,见
|
|
86
|
+
* `WebViewHost.setMenuItemState`),拿不到另一维就会把用户刚点出来的勾选抹掉。
|
|
87
|
+
*/
|
|
88
|
+
public func findById(id: String): Option<MenuItem> {
|
|
89
|
+
return MenuModel.lookup(this.items, id)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private static func lookup(items: ArrayList<MenuItem>, id: String): Option<MenuItem> {
|
|
93
|
+
for (item in items) {
|
|
94
|
+
if (item.id == id) {
|
|
95
|
+
return Some(item)
|
|
96
|
+
}
|
|
97
|
+
match (MenuModel.lookup(item.children, id)) {
|
|
98
|
+
case Some(found) => return Some(found)
|
|
99
|
+
case None => ()
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return None
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* 线路格式(RFC-002 §5.4):一行一项,字段用制表符分隔,**行首制表符数 = 层级**。
|
|
107
|
+
*
|
|
108
|
+
* <tabs><kind>\t<id>\t<label>\t<flags>\t<accel>\n
|
|
109
|
+
*
|
|
110
|
+
* kind:`n` 普通 / `c` 勾选 / `s` 分隔线 / `m` 子菜单;
|
|
111
|
+
* flags:十进制,bit0 = 可用、bit1 = 勾选(`3` = 又可用又勾选);
|
|
112
|
+
* 分隔线与子菜单也走满 5 个字段(id / label / accel 为空),C 侧因此不必为它们开特例。
|
|
113
|
+
*
|
|
114
|
+
* 为什么 kind 放最前:C 侧先分派再取值,日志里一眼能看出这行是什么。
|
|
115
|
+
* 为什么不是 JSON:C 侧(bridge_core.c)是纯 C、不引第三方,行式文本解析几十行就够,
|
|
116
|
+
* 且坏了能直接 `fprintf` 到 stderr(诊断走 stderr 是项目铁律)。JSON 留在仓颉侧给 CLI / 清单用。
|
|
117
|
+
* 为什么不做转义:字段里出现制表符 / 换行会破坏「一行一项」的结构——v1 的口径是
|
|
118
|
+
* **丢弃该项 + stderr 逐项提示**(见 emit),而不是引入转义/反转义这一对必须同时正确的函数。
|
|
119
|
+
*/
|
|
120
|
+
public func toWire(): String {
|
|
121
|
+
return MenuModel.emit(this.items, 0)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** 包内共享(TrayModel 的托盘菜单复用同一遍发射器,别再抄一份格式) */
|
|
125
|
+
static func emit(items: ArrayList<MenuItem>, depth: Int64): String {
|
|
126
|
+
var out = ""
|
|
127
|
+
for (item in items) {
|
|
128
|
+
if (!MenuModel.wireSafe(item.id) || !MenuModel.wireSafe(item.label) ||
|
|
129
|
+
!MenuModel.wireSafe(item.accelerator)) {
|
|
130
|
+
eprintln("[cj-tauri] menu: item dropped from wire (id/label/accelerator contains tab or newline)")
|
|
131
|
+
continue
|
|
132
|
+
}
|
|
133
|
+
out += MenuModel.tabs(depth) + MenuModel.kindCode(item.kind) + "\t" + item.id + "\t" +
|
|
134
|
+
item.label + "\t" + MenuModel.flags(item).toString() + "\t" + item.accelerator + "\n"
|
|
135
|
+
if (MenuModel.isSubmenu(item.kind)) {
|
|
136
|
+
out += MenuModel.emit(item.children, depth + 1)
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return out
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* 枚举值**不能用 `==` 比较**(实测:Cangjie 1.2.0 编译期报
|
|
144
|
+
* `invalid binary operator '==' on type 'Enum-MenuItemKind' and 'Enum-MenuItemKind'`),只有 match 能判。
|
|
145
|
+
*/
|
|
146
|
+
private static func isSubmenu(kind: MenuItemKind): Bool {
|
|
147
|
+
match (kind) {
|
|
148
|
+
case MenuItemKind.Submenu => true
|
|
149
|
+
case _ => false
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* 字段里不许有制表符 / 换行 / 回车:它们是线路格式的结构字符。
|
|
155
|
+
* 注意本 SDK 里 `for (cp in someString)` 迭代出的是 **UInt8 字节**(不是 UInt32 码点,AGENTS §4 那条已更正):
|
|
156
|
+
* 三个结构字符都在 ASCII 范围内,按字节比对既够用、也不受多字节字符影响。
|
|
157
|
+
*/
|
|
158
|
+
private static func wireSafe(s: String): Bool {
|
|
159
|
+
for (cp in s) {
|
|
160
|
+
if (cp == 9u8 || cp == 10u8 || cp == 13u8) {
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return true
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private static func tabs(n: Int64): String {
|
|
168
|
+
var out = ""
|
|
169
|
+
var i = 0
|
|
170
|
+
while (i < n) {
|
|
171
|
+
out += "\t"
|
|
172
|
+
i++
|
|
173
|
+
}
|
|
174
|
+
return out
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private static func kindCode(kind: MenuItemKind): String {
|
|
178
|
+
match (kind) {
|
|
179
|
+
case MenuItemKind.Normal => "n"
|
|
180
|
+
case MenuItemKind.Checkbox => "c"
|
|
181
|
+
case MenuItemKind.Separator => "s"
|
|
182
|
+
case MenuItemKind.Submenu => "m"
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** flags:bit0 = 可用、bit1 = 勾选(十进制传输,C 侧只要 atoi) */
|
|
187
|
+
private static func flags(item: MenuItem): Int64 {
|
|
188
|
+
var f = 0
|
|
189
|
+
if (item.enabled) {
|
|
190
|
+
f += 1
|
|
191
|
+
}
|
|
192
|
+
if (item.checked) {
|
|
193
|
+
f += 2
|
|
194
|
+
}
|
|
195
|
+
return f
|
|
196
|
+
}
|
|
197
|
+
}
|
package/src/plugin.cj
CHANGED
|
@@ -45,6 +45,17 @@ public interface Plugin {
|
|
|
45
45
|
func permissions(): HashMap<String, ArrayList<String>> {
|
|
46
46
|
return HashMap<String, ArrayList<String>>()
|
|
47
47
|
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 本插件是否吃「路径 scope」这层校验(RFC-003 第 4 期,默认 false)。
|
|
51
|
+
*
|
|
52
|
+
* 声明 true 意味着:命令里带路径的参数一律经 `IpcContext.resolveScopedPath(插件名, 路径)` 取值
|
|
53
|
+
* (越界即拒),且清单没为它写 scopes 时框架会在启动时提示「按旧行为全量放行」。
|
|
54
|
+
* 为什么不由框架猜「哪个参数是路径」:只有插件自己知道参数语义,猜出来的白名单等于没有白名单。
|
|
55
|
+
*/
|
|
56
|
+
func scopeAware(): Bool {
|
|
57
|
+
return false
|
|
58
|
+
}
|
|
48
59
|
}
|
|
49
60
|
|
|
50
61
|
/**
|
|
@@ -257,3 +268,32 @@ public func warnUnauthorizedPlugins(plugins: ArrayList<Plugin>, caps: Capability
|
|
|
257
268
|
}
|
|
258
269
|
}
|
|
259
270
|
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* 提示「吃 scope 的插件没有被声明 scope」——旧清单缺省行为:全量放行(RFC-003 第 4 期)。
|
|
274
|
+
*
|
|
275
|
+
* 只在「该插件至少有一条命令被授权」时提示:没用到它的应用不必被这行日志打扰。
|
|
276
|
+
* 声明了 scope 的插件(caps.hasScopeForPlugin 为真)不提示。
|
|
277
|
+
*/
|
|
278
|
+
public func warnUnscopedPlugins(plugins: ArrayList<Plugin>, caps: CapabilityRegistry): Unit {
|
|
279
|
+
for (p in plugins) {
|
|
280
|
+
if (!p.scopeAware()) {
|
|
281
|
+
continue
|
|
282
|
+
}
|
|
283
|
+
var authorized = false
|
|
284
|
+
for (name in pluginCommandNames(p)) {
|
|
285
|
+
if (caps.canInvoke(name)) {
|
|
286
|
+
authorized = true
|
|
287
|
+
break
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!authorized) {
|
|
291
|
+
continue
|
|
292
|
+
}
|
|
293
|
+
if (caps.hasScopeForPlugin(p.name())) {
|
|
294
|
+
continue
|
|
295
|
+
}
|
|
296
|
+
eprintln("[cj-tauri] 插件 ${p.name()} 的路径参数未声明 scope:按旧行为全量放行")
|
|
297
|
+
eprintln("[cj-tauri] 建议在 capabilities 的 json 里加 \"scopes\": { \"${p.name()}\": { \"allow\": [\"<放行路径>\"] } }")
|
|
298
|
+
}
|
|
299
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
package cjTauri.plugins.clipboard
|
|
2
|
+
|
|
3
|
+
import cjTauri.*
|
|
4
|
+
|
|
5
|
+
import std.collection.*
|
|
6
|
+
import stdx.encoding.json.*
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 官方插件 clipboard:系统剪贴板(对标 Tauri 的 clipboard-manager 插件,RFC-003 第 3 期)
|
|
10
|
+
*
|
|
11
|
+
* 命令:clipboard:write / clipboard:read
|
|
12
|
+
* 权限:**不自动放行**,应用要在 capabilities 里显式声明,两种写法等价:
|
|
13
|
+
* "commands": ["clipboard:read"] // 明文逐条
|
|
14
|
+
* "permissions": ["clipboard:readonly"] // 命名权限集(只放行 read)
|
|
15
|
+
* "permissions": ["clipboard:default"] // 命名权限集(读写全量)
|
|
16
|
+
* 前端:window.__CJ_TAURI__.clipboard.writeText("...") / readText()
|
|
17
|
+
*
|
|
18
|
+
* 真正的读写落在 C 桥(`cj_bridge_clipboard_*`):GTK / Win32 的剪贴板调用都必须在
|
|
19
|
+
* 宿主 UI 线程上,core 那边做「UI 线程分流 + 单槽等待」。插件只做参数校验与命令分发,
|
|
20
|
+
* 平台差异不进插件(AGENTS §2 的架构契约)。
|
|
21
|
+
*
|
|
22
|
+
* 读写都要宿主已就绪(start 之后);更早调用得到 false / None。
|
|
23
|
+
*/
|
|
24
|
+
public class ClipboardPlugin <: Plugin {
|
|
25
|
+
private let handler: ClipboardCommands = ClipboardCommands()
|
|
26
|
+
|
|
27
|
+
public init() {}
|
|
28
|
+
|
|
29
|
+
public func name(): String {
|
|
30
|
+
return "clipboard"
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public func commands(): HashMap<String, CommandHandler> {
|
|
34
|
+
let m = HashMap<String, CommandHandler>()
|
|
35
|
+
m.add("write", this.handler)
|
|
36
|
+
m.add("read", this.handler)
|
|
37
|
+
return m
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* 命名权限集(v1.1):readonly 只放行读——剪贴板里可能有用户敏感内容,
|
|
42
|
+
* 「只写不读」是合理的最小授权面。
|
|
43
|
+
*/
|
|
44
|
+
public func permissions(): HashMap<String, ArrayList<String>> {
|
|
45
|
+
let m = HashMap<String, ArrayList<String>>()
|
|
46
|
+
let ro = ArrayList<String>()
|
|
47
|
+
ro.add("read")
|
|
48
|
+
m.add("readonly", ro)
|
|
49
|
+
let all = ArrayList<String>()
|
|
50
|
+
all.add("read")
|
|
51
|
+
all.add("write")
|
|
52
|
+
m.add("default", all)
|
|
53
|
+
return m
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 前端 shim:往桥对象上挂一个 clipboard 命名空间(与 dialog 同款)
|
|
58
|
+
*/
|
|
59
|
+
public func jsShim(): String {
|
|
60
|
+
return "(function () { var t = window.__CJ_TAURI__ || (window.__CJ_TAURI__ = {});" +
|
|
61
|
+
" t.clipboard = {" +
|
|
62
|
+
" writeText: function (text) { return window.__CJ_TAURI__.invoke('clipboard:write', { text: text }); }," +
|
|
63
|
+
" readText: function () { return window.__CJ_TAURI__.invoke('clipboard:read', {}); }" +
|
|
64
|
+
" }; })();\n"
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
public func setup(app: TauriApp): Unit {
|
|
68
|
+
// 多窗口按报文里的 label 解析宿主(与 dialog 插件同一口径)。固定 `app.webviewHost()`
|
|
69
|
+
// 会让第二扇窗的剪贴板命令全部落到默认窗那一个宿主上(2026-10-20 dialog 实机取证同款)。
|
|
70
|
+
this.handler.attachHostResolver({ label => app.hostOf(label) })
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* 剪贴板命令处理器:write 收 { text },read 无参。
|
|
76
|
+
*/
|
|
77
|
+
public class ClipboardCommands <: CommandHandler {
|
|
78
|
+
private var host: Option<WebViewHost> = None
|
|
79
|
+
|
|
80
|
+
/* 按 label 解析宿主(多窗口的正路):单槽 `host` 只记着装配时那一个宿主,
|
|
81
|
+
多窗口下用它就是「所有窗的剪贴板命令都发给默认窗那一个宿主」。 */
|
|
82
|
+
private var hostByWindow: Option<(String) -> Option<WebViewHost>> = None
|
|
83
|
+
|
|
84
|
+
public init() {}
|
|
85
|
+
|
|
86
|
+
/** 由 ClipboardPlugin.setup() 调用(单窗口 / 单测用;这个槽不带窗口身份) */
|
|
87
|
+
public func attachHost(h: WebViewHost): Unit {
|
|
88
|
+
this.host = Some(h)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** 装配「label → 宿主」解析器;由 ClipboardPlugin.setup() 传入 `app.hostOf` */
|
|
92
|
+
public func attachHostResolver(resolve: (String) -> Option<WebViewHost>): Unit {
|
|
93
|
+
this.hostByWindow = Some(resolve)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
public func handle(cmd: String, args: JsonObject, ipc: IpcContext): JsonValue {
|
|
97
|
+
match (cmd) {
|
|
98
|
+
case "clipboard:write" =>
|
|
99
|
+
let text = this.requireString(args, "text")
|
|
100
|
+
if (this.requireHost(ipc).clipboardWriteText(text)) {
|
|
101
|
+
return JsonNull()
|
|
102
|
+
}
|
|
103
|
+
throw CommandException("clipboard: 写入失败(宿主未就绪 / 平台拒绝)")
|
|
104
|
+
case "clipboard:read" =>
|
|
105
|
+
match (this.requireHost(ipc).clipboardReadText()) {
|
|
106
|
+
case Some(text) => return JsonString(text)
|
|
107
|
+
case None => return JsonNull() // 空 / 非文本:合法结果,不是错误
|
|
108
|
+
}
|
|
109
|
+
case _ => throw CommandException("unknown clipboard command: ${cmd}")
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* 取**发起这条命令的窗口**的宿主
|
|
115
|
+
*
|
|
116
|
+
* 多窗口下必须按 `ipc.window` 解析:用装配时那一个宿主,第二扇窗的命令会落到默认窗的宿主上。
|
|
117
|
+
* 解析器在场而窗口没登记时**如实报错**,不回落到单槽(与 dialog 插件同一口径)。
|
|
118
|
+
*/
|
|
119
|
+
private func requireHost(ipc: IpcContext): WebViewHost {
|
|
120
|
+
match (this.hostByWindow) {
|
|
121
|
+
case Some(resolve) => match (resolve(ipc.window)) {
|
|
122
|
+
case Some(h) => return h
|
|
123
|
+
case None => throw CommandException(
|
|
124
|
+
"clipboard: 窗口 \"${ipc.window}\" 没有登记宿主(addWindow(); 默认窗在 run() 之前也可用)")
|
|
125
|
+
}
|
|
126
|
+
case None => ()
|
|
127
|
+
}
|
|
128
|
+
match (this.host) {
|
|
129
|
+
case Some(h) => h
|
|
130
|
+
case None => throw CommandException(
|
|
131
|
+
"clipboard: 宿主未装配(用 .plugin(ClipboardPlugin()) 装配插件后才可用)")
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
private func requireString(args: JsonObject, key: String): String {
|
|
136
|
+
match (args.get(key)) {
|
|
137
|
+
case Some(v) => match (v.kind()) {
|
|
138
|
+
case JsonKind.JsString => return v.asString().getValue()
|
|
139
|
+
case _ => throw CommandException("clipboard: argument \"${key}\" must be a string")
|
|
140
|
+
}
|
|
141
|
+
case None => throw CommandException("clipboard: missing argument \"${key}\"")
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|