cj-tauri 1.1.2 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +137 -77
- package/bin/cj-tauri.js +287 -287
- package/cjpm.toml +13 -2
- package/cli/cjpm.toml +22 -13
- package/cli/src/doctor.cj +308 -304
- package/cli/src/main.cj +129 -121
- package/cli/src/manifest.cj +632 -0
- package/cli/src/manifest_test.cj +87 -0
- package/cli/src/project.cj +770 -754
- package/cli/src/resolve.cj +348 -348
- package/cli/src/scaffold.cj +46 -38
- package/cli/src/sync.cj +526 -0
- package/cli/src/util.cj +9 -0
- package/cli/templates/app/README.md +30 -59
- package/cli/templates/app/{ui → assets/ui}/index.html +2 -0
- package/cli/templates/app/cj-tauri.toml +32 -0
- package/cli/templates/app/cjpm.toml +21 -4
- package/cli/templates/app/src/main.cj +29 -16
- package/cli/templates/app-react/README.md +34 -45
- package/cli/templates/app-react/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-react/{ui → assets/ui}/src/App.css +67 -67
- package/cli/templates/app-react/{ui → assets/ui}/src/App.jsx +124 -124
- package/cli/templates/app-react/cj-tauri.toml +33 -0
- package/cli/templates/app-react/cjpm.toml +21 -4
- package/cli/templates/app-react/src/main.cj +18 -29
- package/cli/templates/app-vue/README.md +34 -38
- package/cli/templates/app-vue/{ui → assets/ui}/index.html +25 -25
- package/cli/templates/app-vue/{ui → assets/ui}/src/App.vue +176 -176
- package/cli/templates/app-vue/cj-tauri.toml +33 -0
- package/cli/templates/app-vue/cjpm.toml +21 -4
- package/cli/templates/app-vue/src/main.cj +18 -29
- package/cli/templates/{ohos-pc → ohos-skeleton}/README.md +5 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/build-hap.sh +15 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/cjpm.toml +8 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/dialog_host.cj +217 -0
- package/cli/templates/ohos-skeleton/entry/src/main/cangjie/index.cj +527 -0
- package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/module.json5 +8 -0
- package/native/build_win.bat +62 -62
- package/native/core/bridge_core.c +1681 -1626
- package/native/core/bridge_core.h +675 -618
- package/native/core/bridge_js.h +108 -108
- package/native/platforms/harmonyos/README.md +115 -6
- package/native/platforms/linux/bridge_linux.c +2787 -2485
- package/native/platforms/windows/bridge_win.c +3740 -3248
- package/native/tests/gtk_threading_probe.c +143 -143
- package/native/tests/test_bridge_core.c +1339 -1228
- package/package.json +53 -51
- package/prebuilt/linux-x64/cj-tauri +0 -0
- package/src/api_system.cj +67 -67
- package/src/app.cj +325 -940
- package/src/app_events.cj +166 -0
- package/src/app_host_ops.cj +287 -0
- package/src/app_run.cj +283 -0
- package/src/app_spec.cj +76 -0
- package/src/arkweb_menu.cj +216 -0
- package/src/arkweb_script.cj +0 -12
- package/src/capability.cj +409 -409
- package/src/clipboard_bridge.cj +178 -0
- package/src/dialog_spec.cj +79 -0
- package/src/dragdrop.cj +26 -26
- package/src/embedded_assets.cj +98 -98
- package/src/host.cj +752 -604
- package/src/host_arkweb.cj +201 -17
- package/src/host_webkit.cj +663 -548
- package/src/host_webview2.cj +663 -548
- package/src/hotkey.cj +66 -66
- package/src/ipc_hub.cj +337 -337
- package/src/ipc_message.cj +286 -286
- package/src/launch.cj +62 -0
- package/src/logging.cj +127 -127
- package/src/menu.cj +197 -197
- package/src/platform/platform_app_context.cj +77 -0
- package/src/platform/platform_assets.cj +33 -0
- package/src/platform/platform_discover.cj +176 -0
- package/src/platform/platform_env.cj +29 -0
- package/src/platform/platform_feature.cj +31 -0
- package/src/platform/platform_page_source.cj +87 -0
- package/src/platform/platform_target_info.cj +32 -0
- package/src/platform_host_provider.cj +87 -0
- package/src/plugin.cj +333 -299
- package/src/plugins/clipboard/plugin_clipboard.cj +152 -144
- package/src/plugins/dialog/plugin_dialog.cj +209 -209
- package/src/plugins/fs/plugin_fs.cj +105 -1
- package/src/plugins/http/http_backend.cj +131 -0
- package/src/plugins/http/http_desktop.cj +195 -0
- package/src/plugins/http/http_ohos.cj +204 -0
- package/src/plugins/http/plugin_http.cj +310 -459
- package/src/plugins/log/plugin_log.cj +105 -105
- package/src/plugins/menu/plugin_menu.cj +8 -0
- package/src/plugins/notification/plugin_notification.cj +154 -146
- package/src/plugins/serial/plugin_serial.cj +17 -1
- package/src/plugins/serial/serial_bridge.cj +9 -1
- package/src/plugins/serial/serial_manager_ohos.cj +92 -0
- package/src/plugins/serial/serial_ohos.cj +529 -0
- package/src/plugins/shell/plugin_shell.cj +29 -1
- package/src/plugins/sqlite/plugin_sqlite.cj +8 -0
- package/src/plugins/ws/plugin_ws.cj +488 -470
- package/src/process_hidden.cj +174 -174
- package/src/scope.cj +391 -391
- package/src/shell_event.cj +69 -69
- package/src/tests/app_spec_test.cj +126 -0
- package/src/tests/capability_test.cj +243 -243
- package/src/tests/dialog_spec_test.cj +73 -0
- package/src/tests/fake_host.cj +201 -186
- package/src/tests/ipc_hub_test.cj +487 -487
- package/src/tests/logging_test.cj +62 -62
- package/src/tests/platform_contract_test.cj +251 -0
- package/src/tests/plugin_clipboard_test.cj +222 -222
- package/src/tests/plugin_dialog_test.cj +237 -237
- package/src/tests/plugin_fs_test.cj +207 -0
- package/src/tests/plugin_http_test.cj +124 -124
- package/src/tests/plugin_notification_test.cj +205 -205
- package/src/tests/plugin_serial_test.cj +48 -0
- package/src/tests/plugin_test.cj +66 -3
- package/src/tests/plugin_ws_test.cj +40 -40
- package/src/tests/process_hidden_test.cj +129 -129
- package/src/tests/scope_test.cj +350 -350
- package/src/tests/system_host_test.cj +99 -81
- package/src/tests/urlutil_test.cj +53 -53
- package/src/tests/version_test.cj +75 -75
- package/src/tests/window_registry_test.cj +123 -123
- package/src/tray.cj +90 -90
- package/src/urlutil.cj +159 -159
- package/src/version.cj +44 -44
- package/src/window.cj +51 -51
- package/src/window_registry.cj +116 -116
- package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +0 -167
- package/prebuilt/win32-x64/cj-tauri.exe +0 -0
- package/src/host_factory.cj +0 -48
- /package/cli/templates/app/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/src/main.jsx +0 -0
- /package/cli/templates/app-react/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/app-vue/{capabilities → assets/capabilities}/default.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/package.json +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/src/main.js +0 -0
- /package/cli/templates/app-vue/{ui → assets/ui}/vite.config.js +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/app.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/AppScope/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/build-profile.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/hvigorfile.ts +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/oh-package.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/ability_stage.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/app_host.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/cangjie/main_ability.cj +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/color.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/element/string.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/background.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/foreground.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/layered_image.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/media/startIcon.png +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/base/profile/main_pages.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/capabilities.json +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/entry/src/main/resources/rawfile/index.html +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigor/hvigor-config.json5 +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/hvigorfile.ts +0 -0
- /package/cli/templates/{ohos-pc → ohos-skeleton}/oh-package.json5 +0 -0
package/src/logging.cj
CHANGED
|
@@ -1,127 +1,127 @@
|
|
|
1
|
-
package cjTauri
|
|
2
|
-
|
|
3
|
-
import std.env.*
|
|
4
|
-
import std.convert.*
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* 日志落盘的策略层(RFC-003 第 5 期):文件放哪、预算多少、环境变量怎么兜底。
|
|
8
|
-
*
|
|
9
|
-
* 落盘本身在 `src/log_bridge.cj`(进程级、无平台分支);这里只管策略,纯函数 + `@When` 目录推导。
|
|
10
|
-
*
|
|
11
|
-
* 为什么要环境变量兜底:单文件产物是 Windows 的 GUI 子系统、双击时没有控制台,而**已经分发的
|
|
12
|
-
* exe 改不了代码**——让报障的人带上 `CJ_TAURI_LOG` 再启动一次,就能拿到凭证;
|
|
13
|
-
* 不设这些变量时行为与之前**完全一致**(不落盘、stderr 照旧)。
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/** 默认预算:256 KB 一份、留 2 份档案(最坏约 1 MB,常驻托盘类应用也吃得消) */
|
|
17
|
-
public let CJ_LOG_DEFAULT_MAX_KB: Int64 = 256
|
|
18
|
-
public let CJ_LOG_DEFAULT_KEEP: Int64 = 2
|
|
19
|
-
|
|
20
|
-
/** 应用名缺省值(应用没给名字时用它当文件名) */
|
|
21
|
-
public let CJ_LOG_DEFAULT_APP: String = "app"
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* 单行上限(字节):页面可控的字符串,超了只记一条说明
|
|
25
|
-
*
|
|
26
|
-
* 8 KiB 够放一段异常栈;不设上限的话,一行天文数字能同时把内存与日志文件撑爆,
|
|
27
|
-
* 而这一行本身还未必有诊断价值。
|
|
28
|
-
*/
|
|
29
|
-
public let CJ_LOG_MAX_LINE: Int64 = 8192
|
|
30
|
-
|
|
31
|
-
/** 读环境变量(空串按未设置算)。公开只为子包用例可见(子包看不到父包非 public 符号) */
|
|
32
|
-
public func logEnvOr(name: String, dflt: String): String {
|
|
33
|
-
if (let Some(v) <- getVariable(name)) {
|
|
34
|
-
if (v.size > 0) {
|
|
35
|
-
return v
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
return dflt
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* 解析预算类环境变量(CJ_TAURI_LOG_MAX_KB / CJ_TAURI_LOG_KEEP)
|
|
43
|
-
*
|
|
44
|
-
* 非法值按未设置算,并留一行 stderr——「设了但没生效」比「没设」难查得多。
|
|
45
|
-
* 公开只为子包用例可见(子包看不到父包非 public 符号)
|
|
46
|
-
*/
|
|
47
|
-
public func logBudgetFromEnv(name: String, dflt: Int64): Int64 {
|
|
48
|
-
if (let Some(v) <- getVariable(name)) {
|
|
49
|
-
if (v.size > 0) {
|
|
50
|
-
try {
|
|
51
|
-
let n = Int64.parse(v)
|
|
52
|
-
if (n > 0) {
|
|
53
|
-
return n
|
|
54
|
-
}
|
|
55
|
-
eprintln("[cj-tauri] ${name}=\"${v}\" 不是正整数,按默认值 ${dflt} 处理")
|
|
56
|
-
} catch (e: Exception) {
|
|
57
|
-
eprintln("[cj-tauri] ${name}=\"${v}\" 解析失败,按默认值 ${dflt} 处理")
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
return dflt
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* 平台惯例的日志目录(不含文件名)
|
|
66
|
-
*
|
|
67
|
-
* Linux 认 `XDG_STATE_HOME`(缺省 `~/.local/state`):日志属 state,不属 config——
|
|
68
|
-
* 放 `~/.config` 会把「可丢弃的运行痕迹」混进配置备份里。
|
|
69
|
-
*/
|
|
70
|
-
@When[os == "Linux"]
|
|
71
|
-
public func defaultLogDir(): String {
|
|
72
|
-
let base = logEnvOr("XDG_STATE_HOME", "")
|
|
73
|
-
if (base.size > 0) {
|
|
74
|
-
return "${base}/cj-tauri"
|
|
75
|
-
}
|
|
76
|
-
return "${logEnvOr("HOME", "/tmp")}/.local/state/cj-tauri"
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* 平台惯例的日志目录(不含文件名)
|
|
81
|
-
*
|
|
82
|
-
* Windows 认 `%LOCALAPPDATA%`(`C:\Users\<用户>\AppData\Local`):单机可丢的数据就该落这里,
|
|
83
|
-
* 漫游配置里不放日志。变量缺失时退到 USERPROFILE,别拼出个以 `\cj-tauri` 开头的绝对路径。
|
|
84
|
-
*/
|
|
85
|
-
@When[os == "Windows"]
|
|
86
|
-
public func defaultLogDir(): String {
|
|
87
|
-
let base = logEnvOr("LOCALAPPDATA", "")
|
|
88
|
-
if (base.size > 0) {
|
|
89
|
-
return "${base}\\cj-tauri\\logs"
|
|
90
|
-
}
|
|
91
|
-
return "${logEnvOr("USERPROFILE", "C:")}\\AppData\\Local\\cj-tauri\\logs"
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/** 目录 + 文件名:分隔符按平台给(C 侧整条路径直接用,不做拼接) */
|
|
95
|
-
@When[os == "Linux"]
|
|
96
|
-
func logPathJoin(dir: String, name: String): String {
|
|
97
|
-
return "${dir}/${name}"
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** 目录 + 文件名:分隔符按平台给(C 侧整条路径直接用,不做拼接) */
|
|
101
|
-
@When[os == "Windows"]
|
|
102
|
-
func logPathJoin(dir: String, name: String): String {
|
|
103
|
-
return "${dir}\\${name}"
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/** 应用名 → 平台惯例目录下的日志全路径(`CJ_TAURI_LOG` 的覆写由调用方先判) */
|
|
107
|
-
public func defaultLogPath(appName: String): String {
|
|
108
|
-
let name = if (appName.size > 0) {
|
|
109
|
-
appName
|
|
110
|
-
} else {
|
|
111
|
-
CJ_LOG_DEFAULT_APP
|
|
112
|
-
}
|
|
113
|
-
return logPathJoin(defaultLogDir(), "${name}.log")
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* 「环境变量给了就用它,没给才落平台惯例目录」——`setLogToDefaultFile` 的选择器
|
|
118
|
-
*
|
|
119
|
-
* 抽成纯函数只为**可测**:单测里改环境变量会污染同进程的其它用例,所以把「读到什么」
|
|
120
|
-
* 当参数传进来,判据钉在这个函数上(`envPath` 空串=未设置,与 `logEnvOr` 同口径)。
|
|
121
|
-
*/
|
|
122
|
-
public func logPathOrDefault(envPath: String, appName: String): String {
|
|
123
|
-
if (envPath.size > 0) {
|
|
124
|
-
return envPath
|
|
125
|
-
}
|
|
126
|
-
return defaultLogPath(appName)
|
|
127
|
-
}
|
|
1
|
+
package cjTauri
|
|
2
|
+
|
|
3
|
+
import std.env.*
|
|
4
|
+
import std.convert.*
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 日志落盘的策略层(RFC-003 第 5 期):文件放哪、预算多少、环境变量怎么兜底。
|
|
8
|
+
*
|
|
9
|
+
* 落盘本身在 `src/log_bridge.cj`(进程级、无平台分支);这里只管策略,纯函数 + `@When` 目录推导。
|
|
10
|
+
*
|
|
11
|
+
* 为什么要环境变量兜底:单文件产物是 Windows 的 GUI 子系统、双击时没有控制台,而**已经分发的
|
|
12
|
+
* exe 改不了代码**——让报障的人带上 `CJ_TAURI_LOG` 再启动一次,就能拿到凭证;
|
|
13
|
+
* 不设这些变量时行为与之前**完全一致**(不落盘、stderr 照旧)。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/** 默认预算:256 KB 一份、留 2 份档案(最坏约 1 MB,常驻托盘类应用也吃得消) */
|
|
17
|
+
public let CJ_LOG_DEFAULT_MAX_KB: Int64 = 256
|
|
18
|
+
public let CJ_LOG_DEFAULT_KEEP: Int64 = 2
|
|
19
|
+
|
|
20
|
+
/** 应用名缺省值(应用没给名字时用它当文件名) */
|
|
21
|
+
public let CJ_LOG_DEFAULT_APP: String = "app"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* 单行上限(字节):页面可控的字符串,超了只记一条说明
|
|
25
|
+
*
|
|
26
|
+
* 8 KiB 够放一段异常栈;不设上限的话,一行天文数字能同时把内存与日志文件撑爆,
|
|
27
|
+
* 而这一行本身还未必有诊断价值。
|
|
28
|
+
*/
|
|
29
|
+
public let CJ_LOG_MAX_LINE: Int64 = 8192
|
|
30
|
+
|
|
31
|
+
/** 读环境变量(空串按未设置算)。公开只为子包用例可见(子包看不到父包非 public 符号) */
|
|
32
|
+
public func logEnvOr(name: String, dflt: String): String {
|
|
33
|
+
if (let Some(v) <- getVariable(name)) {
|
|
34
|
+
if (v.size > 0) {
|
|
35
|
+
return v
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return dflt
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 解析预算类环境变量(CJ_TAURI_LOG_MAX_KB / CJ_TAURI_LOG_KEEP)
|
|
43
|
+
*
|
|
44
|
+
* 非法值按未设置算,并留一行 stderr——「设了但没生效」比「没设」难查得多。
|
|
45
|
+
* 公开只为子包用例可见(子包看不到父包非 public 符号)
|
|
46
|
+
*/
|
|
47
|
+
public func logBudgetFromEnv(name: String, dflt: Int64): Int64 {
|
|
48
|
+
if (let Some(v) <- getVariable(name)) {
|
|
49
|
+
if (v.size > 0) {
|
|
50
|
+
try {
|
|
51
|
+
let n = Int64.parse(v)
|
|
52
|
+
if (n > 0) {
|
|
53
|
+
return n
|
|
54
|
+
}
|
|
55
|
+
eprintln("[cj-tauri] ${name}=\"${v}\" 不是正整数,按默认值 ${dflt} 处理")
|
|
56
|
+
} catch (e: Exception) {
|
|
57
|
+
eprintln("[cj-tauri] ${name}=\"${v}\" 解析失败,按默认值 ${dflt} 处理")
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return dflt
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* 平台惯例的日志目录(不含文件名)
|
|
66
|
+
*
|
|
67
|
+
* Linux 认 `XDG_STATE_HOME`(缺省 `~/.local/state`):日志属 state,不属 config——
|
|
68
|
+
* 放 `~/.config` 会把「可丢弃的运行痕迹」混进配置备份里。
|
|
69
|
+
*/
|
|
70
|
+
@When[os == "Linux"]
|
|
71
|
+
public func defaultLogDir(): String {
|
|
72
|
+
let base = logEnvOr("XDG_STATE_HOME", "")
|
|
73
|
+
if (base.size > 0) {
|
|
74
|
+
return "${base}/cj-tauri"
|
|
75
|
+
}
|
|
76
|
+
return "${logEnvOr("HOME", "/tmp")}/.local/state/cj-tauri"
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 平台惯例的日志目录(不含文件名)
|
|
81
|
+
*
|
|
82
|
+
* Windows 认 `%LOCALAPPDATA%`(`C:\Users\<用户>\AppData\Local`):单机可丢的数据就该落这里,
|
|
83
|
+
* 漫游配置里不放日志。变量缺失时退到 USERPROFILE,别拼出个以 `\cj-tauri` 开头的绝对路径。
|
|
84
|
+
*/
|
|
85
|
+
@When[os == "Windows"]
|
|
86
|
+
public func defaultLogDir(): String {
|
|
87
|
+
let base = logEnvOr("LOCALAPPDATA", "")
|
|
88
|
+
if (base.size > 0) {
|
|
89
|
+
return "${base}\\cj-tauri\\logs"
|
|
90
|
+
}
|
|
91
|
+
return "${logEnvOr("USERPROFILE", "C:")}\\AppData\\Local\\cj-tauri\\logs"
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** 目录 + 文件名:分隔符按平台给(C 侧整条路径直接用,不做拼接) */
|
|
95
|
+
@When[os == "Linux"]
|
|
96
|
+
func logPathJoin(dir: String, name: String): String {
|
|
97
|
+
return "${dir}/${name}"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** 目录 + 文件名:分隔符按平台给(C 侧整条路径直接用,不做拼接) */
|
|
101
|
+
@When[os == "Windows"]
|
|
102
|
+
func logPathJoin(dir: String, name: String): String {
|
|
103
|
+
return "${dir}\\${name}"
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** 应用名 → 平台惯例目录下的日志全路径(`CJ_TAURI_LOG` 的覆写由调用方先判) */
|
|
107
|
+
public func defaultLogPath(appName: String): String {
|
|
108
|
+
let name = if (appName.size > 0) {
|
|
109
|
+
appName
|
|
110
|
+
} else {
|
|
111
|
+
CJ_LOG_DEFAULT_APP
|
|
112
|
+
}
|
|
113
|
+
return logPathJoin(defaultLogDir(), "${name}.log")
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* 「环境变量给了就用它,没给才落平台惯例目录」——`setLogToDefaultFile` 的选择器
|
|
118
|
+
*
|
|
119
|
+
* 抽成纯函数只为**可测**:单测里改环境变量会污染同进程的其它用例,所以把「读到什么」
|
|
120
|
+
* 当参数传进来,判据钉在这个函数上(`envPath` 空串=未设置,与 `logEnvOr` 同口径)。
|
|
121
|
+
*/
|
|
122
|
+
public func logPathOrDefault(envPath: String, appName: String): String {
|
|
123
|
+
if (envPath.size > 0) {
|
|
124
|
+
return envPath
|
|
125
|
+
}
|
|
126
|
+
return defaultLogPath(appName)
|
|
127
|
+
}
|
package/src/menu.cj
CHANGED
|
@@ -1,197 +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
|
-
/** 包内共享(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
|
-
}
|
|
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
|
+
}
|