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.
Files changed (124) hide show
  1. package/README.md +3 -3
  2. package/cjpm.toml +52 -25
  3. package/cli/cjpm.toml +1 -1
  4. package/cli/src/doctor.cj +304 -0
  5. package/cli/src/main.cj +33 -7
  6. package/cli/src/project.cj +59 -1
  7. package/cli/src/resolve.cj +348 -304
  8. package/cli/src/scaffold.cj +139 -22
  9. package/cli/templates/ohos-pc/AppScope/app.json5 +13 -0
  10. package/cli/templates/ohos-pc/AppScope/resources/base/element/string.json +8 -0
  11. package/cli/templates/ohos-pc/AppScope/resources/base/media/background.png +0 -0
  12. package/cli/templates/ohos-pc/AppScope/resources/base/media/foreground.png +0 -0
  13. package/cli/templates/ohos-pc/AppScope/resources/base/media/layered_image.json +6 -0
  14. package/cli/templates/ohos-pc/README.md +79 -0
  15. package/cli/templates/ohos-pc/build-hap.sh +190 -0
  16. package/cli/templates/ohos-pc/build-profile.json5 +46 -0
  17. package/cli/templates/ohos-pc/entry/build-profile.json5 +26 -0
  18. package/cli/templates/ohos-pc/entry/cjpm.toml +43 -0
  19. package/cli/templates/ohos-pc/entry/hvigorfile.ts +6 -0
  20. package/cli/templates/ohos-pc/entry/oh-package.json5 +9 -0
  21. package/cli/templates/ohos-pc/entry/src/main/cangjie/ability_stage.cj +17 -0
  22. package/cli/templates/ohos-pc/entry/src/main/cangjie/app_host.cj +94 -0
  23. package/cli/templates/ohos-pc/entry/src/main/cangjie/index.cj +167 -0
  24. package/cli/templates/ohos-pc/entry/src/main/cangjie/main_ability.cj +139 -0
  25. package/cli/templates/ohos-pc/entry/src/main/module.json5 +49 -0
  26. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/color.json +8 -0
  27. package/cli/templates/ohos-pc/entry/src/main/resources/base/element/string.json +16 -0
  28. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/background.png +0 -0
  29. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/foreground.png +0 -0
  30. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/layered_image.json +6 -0
  31. package/cli/templates/ohos-pc/entry/src/main/resources/base/media/startIcon.png +0 -0
  32. package/cli/templates/ohos-pc/entry/src/main/resources/base/profile/main_pages.json +2 -0
  33. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/capabilities.json +6 -0
  34. package/cli/templates/ohos-pc/entry/src/main/resources/rawfile/index.html +92 -0
  35. package/cli/templates/ohos-pc/hvigor/hvigor-config.json5 +5 -0
  36. package/cli/templates/ohos-pc/hvigorfile.ts +6 -0
  37. package/cli/templates/ohos-pc/oh-package.json5 +10 -0
  38. package/native/build_linux.sh +30 -2
  39. package/native/build_win.bat +62 -61
  40. package/native/core/bridge_core.c +1626 -0
  41. package/native/{bridge_core.h → core/bridge_core.h} +246 -6
  42. package/native/platforms/harmonyos/README.md +106 -0
  43. package/native/platforms/linux/bridge_linux.c +2485 -0
  44. package/native/platforms/windows/bridge_win.c +3248 -0
  45. package/native/tests/test_bridge_core.c +528 -1
  46. package/package.json +12 -3
  47. package/prebuilt/win32-x64/cj-tauri.exe +0 -0
  48. package/src/api_system.cj +10 -2
  49. package/src/app.cj +248 -85
  50. package/src/arkweb_proxy.cj +56 -0
  51. package/src/arkweb_script.cj +154 -0
  52. package/src/capability.cj +140 -0
  53. package/src/clipboard_bridge.cj +55 -0
  54. package/src/dragdrop.cj +26 -0
  55. package/src/embedded_assets.cj +98 -0
  56. package/src/hexutil.cj +53 -0
  57. package/src/host.cj +197 -26
  58. package/src/host_arkweb.cj +740 -0
  59. package/src/host_factory.cj +48 -0
  60. package/src/host_webkit.cj +202 -26
  61. package/src/host_webview2.cj +176 -3
  62. package/src/hotkey.cj +66 -0
  63. package/src/ipc_hub.cj +49 -0
  64. package/src/ipc_message.cj +2 -1
  65. package/src/jsutil.cj +33 -0
  66. package/src/log_bridge.cj +107 -0
  67. package/src/logging.cj +127 -0
  68. package/src/menu.cj +197 -196
  69. package/src/plugin.cj +40 -0
  70. package/src/plugins/clipboard/plugin_clipboard.cj +144 -0
  71. package/src/{plugin_dialog.cj → plugins/dialog/plugin_dialog.cj} +74 -17
  72. package/src/plugins/fs/plugin_fs.cj +194 -0
  73. package/src/plugins/http/plugin_http.cj +459 -0
  74. package/src/plugins/log/plugin_log.cj +105 -0
  75. package/src/{plugin_menu.cj → plugins/menu/plugin_menu.cj} +4 -2
  76. package/src/plugins/mqtt/mqtt_codec.cj +661 -0
  77. package/src/plugins/mqtt/plugin_mqtt.cj +1446 -0
  78. package/src/plugins/notification/plugin_notification.cj +146 -0
  79. package/src/plugins/plugins.cj +5 -0
  80. package/src/plugins/redis/plugin_redis.cj +2409 -0
  81. package/src/plugins/redis/redis_codec.cj +556 -0
  82. package/src/{plugin_serial.cj → plugins/serial/plugin_serial.cj} +442 -489
  83. package/src/{serial_bridge.cj → plugins/serial/serial_bridge.cj} +185 -184
  84. package/src/{plugin_shell.cj → plugins/shell/plugin_shell.cj} +230 -228
  85. package/src/plugins/sqlite/plugin_sqlite.cj +697 -0
  86. package/src/plugins/sqlite/sqlite_ffi.cj +206 -0
  87. package/src/plugins/store/plugin_store.cj +287 -0
  88. package/src/plugins/ws/plugin_ws.cj +470 -0
  89. package/src/process_hidden.cj +174 -0
  90. package/src/scope.cj +391 -0
  91. package/src/shell_event.cj +69 -68
  92. package/src/tests/capability_test.cj +53 -0
  93. package/src/tests/fake_host.cj +85 -20
  94. package/src/tests/ipc_hub_test.cj +13 -0
  95. package/src/tests/jsutil_test.cj +62 -0
  96. package/src/tests/logging_test.cj +62 -0
  97. package/src/tests/mqtt_codec_test.cj +405 -0
  98. package/src/tests/plugin_clipboard_test.cj +222 -0
  99. package/src/tests/plugin_dialog_test.cj +98 -4
  100. package/src/tests/plugin_http_test.cj +124 -0
  101. package/src/tests/plugin_menu_test.cj +1 -0
  102. package/src/tests/plugin_notification_test.cj +205 -0
  103. package/src/tests/plugin_serial_test.cj +269 -267
  104. package/src/tests/plugin_shell_test.cj +189 -188
  105. package/src/tests/plugin_sqlite_test.cj +230 -0
  106. package/src/tests/plugin_store_test.cj +260 -0
  107. package/src/tests/plugin_test.cj +542 -524
  108. package/src/tests/plugin_ws_test.cj +40 -0
  109. package/src/tests/process_hidden_test.cj +129 -0
  110. package/src/tests/redis_codec_test.cj +396 -0
  111. package/src/tests/scope_test.cj +350 -0
  112. package/src/tests/shell_event_test.cj +0 -1
  113. package/src/tests/system_host_test.cj +81 -79
  114. package/src/tests/urlutil_test.cj +53 -0
  115. package/src/tests/version_test.cj +75 -65
  116. package/src/tray.cj +90 -0
  117. package/src/urlutil.cj +159 -0
  118. package/src/version.cj +23 -2
  119. package/src/window.cj +51 -42
  120. package/native/bridge_core.c +0 -797
  121. package/native/bridge_linux.c +0 -1268
  122. package/native/bridge_win.c +0 -1633
  123. package/src/plugin_fs.cj +0 -118
  124. /package/native/{bridge_js.h → core/bridge_js.h} +0 -0
package/src/hotkey.cj ADDED
@@ -0,0 +1,66 @@
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+
5
+ /**
6
+ * 全局快捷键的**纯仓颉模型**(RFC-003 第 5 期首个落地项):只描述意图,不含任何平台概念。
7
+ *
8
+ * 分层与菜单同构(RFC-002 §5.1):模型(本文件)→ `WebViewHost.setGlobalHotkeys()`
9
+ * → C 桥 `cj_bridge_set_global_hotkeys` → 平台翻译层(Linux = X11 grab;Windows 桩如实报暂不支持)。
10
+ *
11
+ * wire 格式(与 `native/core/bridge_core.h` 的 `pending_hotkeys` 注释逐字对应):
12
+ * 一行一条 `<accel>:<id>`,如 `Ctrl+Alt+C:show-panel`。accel 的修饰键只有四个约定名
13
+ * (Ctrl / Alt / Shift / Super),键名是单字符或 F1..F12 / Enter / Esc 等常用键——
14
+ * 平台侧解析不了的那条整行丢弃并出声,而不是静默吞掉。
15
+ *
16
+ * 文本来自**应用作者**(不是页面可控输入),这里不做转义;id 里出现 `:` 或换行是应用自己的错,
17
+ * toWire 不出口这种行(防御性跳过并留一句 stderr 的成本太高,直接约定 + 文档)。
18
+ */
19
+ public class Hotkey {
20
+ public let accel: String
21
+ public let id: String
22
+
23
+ public init(accel: String, id: String) {
24
+ this.accel = accel
25
+ this.id = id
26
+ }
27
+ }
28
+
29
+ /** 快捷键模型:注册表即整表替换(运行期再调 setGlobalHotkeys = 先注销旧表再注册新表) */
30
+ public class HotkeyModel {
31
+ public let keys: ArrayList<Hotkey>
32
+
33
+ public init(keys: ArrayList<Hotkey>) {
34
+ this.keys = keys
35
+ }
36
+
37
+ /** 便捷构造:可变参数式逐条 add */
38
+ public init() {
39
+ this.keys = ArrayList<Hotkey>()
40
+ }
41
+
42
+ public func add(accel: String, id: String): HotkeyModel {
43
+ this.keys.add(Hotkey(accel, id))
44
+ return this
45
+ }
46
+
47
+ /** 线路文本(每行 `<accel>:<id>`;空模型给空串 = 全部注销) */
48
+ public func toWire(): String {
49
+ let sb = StringBuilder()
50
+ var first = true
51
+ for (k in this.keys) {
52
+ if (k.accel.contains(":") || k.accel.contains("\n") ||
53
+ k.id.contains(":") || k.id.contains("\n")) {
54
+ continue // 非法字段整条出局:wire 的分隔符就是这两个字符
55
+ }
56
+ if (!first) {
57
+ sb.append("\n")
58
+ }
59
+ first = false
60
+ sb.append(k.accel)
61
+ sb.append(":")
62
+ sb.append(k.id)
63
+ }
64
+ return sb.toString()
65
+ }
66
+ }
package/src/ipc_hub.cj CHANGED
@@ -72,6 +72,26 @@ public class IpcContext {
72
72
  public func hostCapabilities(): Int32 {
73
73
  return this.hub.hostCapabilities(this.window)
74
74
  }
75
+
76
+ /**
77
+ * 解析并校验一个路径参数(RFC-003 第 4 期)。
78
+ *
79
+ * 带路径参数的插件**一律**用它取代自己拼路径:清单里为插件写了 scope 就按
80
+ * allow/deny 判定,越界抛业务错误;没写就维持旧行为(全量放行,启动时有提示)。
81
+ * 返回值是规范化后的路径(绝对、已解析软链接),直接交给 std.fs 用即可。
82
+ */
83
+ public func resolveScopedPath(plugin: String, rawPath: String): String {
84
+ return this.hub.resolveScopedPath(plugin, rawPath)
85
+ }
86
+
87
+ /**
88
+ * 取某插件的 scope 规则**本体**(不给判定):给「匹配语义不是文件路径」的插件用——
89
+ * 如 http 插件把 allow/deny 模式当**主机名**匹配(不能先走 normalizeScopePath 的
90
+ * 文件路径折叠)。判定本身仍走共享层 `scopeAllows`,插件不自己写第二份 glob。
91
+ */
92
+ public func scopeRuleFor(plugin: String): Option<ScopeRule> {
93
+ return this.hub.scopeRuleFor(plugin)
94
+ }
75
95
  }
76
96
 
77
97
  /**
@@ -123,6 +143,35 @@ public class IpcHub {
123
143
  return this.commands.contains(cmd)
124
144
  }
125
145
 
146
+ /**
147
+ * 路径 scope 校验(RFC-003 第 4 期):IpcContext.resolveScopedPath 的实现体。
148
+ *
149
+ * 校验只在**真的有插件来问**时发生——没有为它声明 scope 的插件完全不受影响(旧清单行为不变)。
150
+ * 拒绝时 stderr 留一行凭证(前缀统一好 grep),前端拿到的是可读的业务错误而不是宿主崩溃。
151
+ */
152
+ public func resolveScopedPath(plugin: String, rawPath: String): String {
153
+ let resolved = normalizeScopePath(rawPath)
154
+ match (this.capabilities.scopeForPlugin(plugin)) {
155
+ case Some(rule) =>
156
+ if (!scopeAllows(rule, resolved)) {
157
+ eprintln("[cj-tauri] scope denied (${plugin}): ${rawPath} -> ${resolved}")
158
+ throw CommandException("${plugin}: path is outside the authorized scope")
159
+ }
160
+ case None => ()
161
+ }
162
+ return resolved
163
+ }
164
+
165
+ /**
166
+ * 取某插件的 scope 规则**本体**(不给判定):给「匹配语义不是文件路径」的插件用——
167
+ * 如 http 插件把 allow/deny 模式当**主机名**匹配(不能先走 normalizeScopePath 的
168
+ * 文件路径折叠,那会把 `api.example.com` 当成相对路径去拼接)。判定本身仍走共享层
169
+ * `scopeAllows`,插件不自己写第二份 glob。
170
+ */
171
+ public func scopeRuleFor(plugin: String): Option<ScopeRule> {
172
+ return this.capabilities.scopeForPlugin(plugin)
173
+ }
174
+
126
175
  /**
127
176
  * 注册后端事件监听器(对标 Tauri Rust 侧的 `app.listen(...)`)
128
177
  *
@@ -8,7 +8,7 @@ import stdx.encoding.json.*
8
8
  * 与桥 JS 的 `windowLabel` 缺省、`emitToWindow(..., "main")` 是同一个约定:老前端与现有示例
9
9
  * 都写死 `"main"`,默认值一错,事件就静默收不到、回执就静默投不回原窗口(见架构文档 §6.4)。
10
10
  */
11
- let DEFAULT_WINDOW_LABEL = "main"
11
+ public let DEFAULT_WINDOW_LABEL = "main"
12
12
 
13
13
  /**
14
14
  * IPC 消息模型(对标 Tauri IPC 协议)
@@ -245,6 +245,7 @@ public class IpcMessage {
245
245
  private static func eventJson(event: String, payload: JsonValue, window: String, coalesce: Bool): JsonObject {
246
246
  var obj = JsonObject()
247
247
  obj.put("type", JsonString("event"))
248
+ obj.put("protocol", JsonString(CJ_IPC_PROTOCOL))
248
249
  obj.put("event", JsonString(event))
249
250
  obj.put("payload", payload)
250
251
  if (coalesce) {
package/src/jsutil.cj ADDED
@@ -0,0 +1,33 @@
1
+ package cjTauri
2
+
3
+ import std.collection.*
4
+
5
+ /**
6
+ * 把文本包成 JS 双引号字符串字面量(核心共享层)
7
+ *
8
+ * ## 为什么单独放在这里,而不是留在 `arkweb_script.cj` 里
9
+ *
10
+ * 调用方 `arkWebLabelScript` 带 `@When[env == "ohos"]` 门禁,桌面 `cjpm test` 根本编不到它;
11
+ * 而这段转义**失效是静默的**:生成的脚本当场语法错误,页面里 `__CJ_TAURI_LABEL__` 永远停在
12
+ * 默认值 `main`,事件过滤失灵却没有一行报错(AGENTS §4 的同族坑:错得不响,查起来最贵)。
13
+ * 放到不带门禁的共享层,桌面单测就能守住它;`public` 是为了跨包单测可直接构造。
14
+ *
15
+ * ## 转义口径
16
+ *
17
+ * 只转义两样:反斜杠与双引号。入参是窗口 label(应用给定的短标识,默认 `main`),
18
+ * 不承载换行等控制字符——**这里不是通用 JSON 编码器**,真要处理控制字符再加分支,
19
+ * 别顺手改成「全量转义」把调用方的语义带偏。
20
+ */
21
+ public func jsStringLiteral(text: String): String {
22
+ let bytes = text.toArray()
23
+ let out = ArrayList<UInt8>(bytes.size + 2)
24
+ out.add(0x22u8)
25
+ for (b in bytes) {
26
+ if (b == 0x5Cu8 || b == 0x22u8) {
27
+ out.add(0x5Cu8)
28
+ }
29
+ out.add(b)
30
+ }
31
+ out.add(0x22u8)
32
+ return String.fromUtf8(out.toArray())
33
+ }
@@ -0,0 +1,107 @@
1
+ package cjTauri
2
+
3
+
4
+ /**
5
+ * 日志落盘桥(RFC-003 第 5 期):C 桥 `cj_bridge_log_*` 的声明与薄包装。
6
+ *
7
+ * 桌面两平台导出同名同签名(AGENTS §2 的硬约束),实现只有一份
8
+ * (`native/core/bridge_core.c`)——落盘是把 fd 2 指到日志文件,与宿主 UI 栈无关,
9
+ * 所以不吃宿主句柄、也不进 `WebViewHost` 接口:它是**进程级**的,不是窗口级的。
10
+ *
11
+ * 为什么非得在 fd 层接管:要让「没有控制台的 GUI 产物」也有凭证,就必须把 C 桥与仓颉侧
12
+ * **两侧**的诊断收进同一份文件,只有 fd 层的接管做得到(见 core 里那段注释的取舍)。
13
+ *
14
+ * 鸿蒙(`env == "ohos"`)侧没有 C 桥产物(`native/platforms/harmonyos/` 目前只有契约),
15
+ * 这三个 FFI 符号无处解析——若照桌面那份写下去,**模块加载期就 SIGSEGV**(实测 2026-10-18:
16
+ * 应用进程 signal:11,AbilityStage.onCreate 一行都执行不到)。所以这里按平台分叉,
17
+ * 鸿蒙侧如实降级:不落盘、`writeLogLine` 返回 false,绝不假装成功。
18
+ *
19
+ * 判据用 `env` 而不是 `arch`(2026-10-19 校正):`arch == "aarch64"` 连桌面 aarch64 Linux
20
+ * 一起门禁掉了,那个平台**是有 C 桥的**,会白白失去日志落盘能力。
21
+ */
22
+
23
+ /** 门禁口径与 `host.cj` 的 FFI 一致:桌面走 C 桥,鸿蒙走降级实现。 */
24
+ @When[env != "ohos"]
25
+ @C
26
+ foreign func cj_bridge_set_log_file(path: CString, maxKB: Int32, keep: Int32): Int32
27
+
28
+ /**
29
+ * 回读生效中的日志路径;未开启时为空串。
30
+ * 返回的是 C 侧内部静态缓冲,**不要**释放(也不要在别的线程上长期持有)。
31
+ */
32
+ @When[env != "ohos"]
33
+ @C
34
+ foreign func cj_bridge_log_file(): CString
35
+
36
+ /**
37
+ * 追加一行到日志出口(同样走轮转):0 = 已写入,-1 = 参数非法。
38
+ * 给 `log` 插件用——页面报错也并进同一份文件(单文件产物里它原本同样看不见)。
39
+ */
40
+ @When[env != "ohos"]
41
+ @C
42
+ foreign func cj_bridge_log_write(line: CString): Int32
43
+
44
+ /** 开日志落盘,返回是否真的开了(被拒时 C 侧已出声,这里绝不假装成功) */
45
+ @When[env != "ohos"]
46
+ func enableLogFile(path: String, maxKB: Int64, keep: Int64): Bool {
47
+ var kb = maxKB
48
+ var kp = keep
49
+ if (kb > 1048576) {
50
+ kb = 1048576 // 1 GiB 上限:Int64 → Int32 的转换不该被一个手滑的环境变量带崩
51
+ }
52
+ if (kp > 1000) {
53
+ kp = 1000
54
+ }
55
+ var rc: Int32 = 0
56
+ unsafe {
57
+ rc = cj_bridge_set_log_file(LibC.mallocCString(path), Int32(kb), Int32(kp))
58
+ }
59
+ return rc == 1
60
+ }
61
+
62
+ /**
63
+ * 鸿蒙侧:没有 C 桥,落盘能力位如实为 0。
64
+ *
65
+ * 返回 false 而不抛异常——调用方(`setLogToDefaultFile`)本就是「尽力而为」语义,
66
+ * 抛异常会把应用启动带走。留一行 stderr 说明「哪条路没走通」。
67
+ */
68
+ @When[env == "ohos"]
69
+ func enableLogFile(path: String, maxKB: Int64, keep: Int64): Bool {
70
+ let _ = maxKB
71
+ let _ = keep
72
+ eprintln("[cj-tauri] log file sink unavailable on this platform (no C bridge): ${path}")
73
+ return false
74
+ }
75
+
76
+ /** 生效中的日志路径(未开启 = 空串)。公开是给插件子包用(子包看不到父包非 public 符号) */
77
+ @When[env != "ohos"]
78
+ public func currentLogFilePath(): String {
79
+ let cs = unsafe { cj_bridge_log_file() }
80
+ if (cs.isNull()) {
81
+ return ""
82
+ }
83
+ return cs.toString()
84
+ }
85
+
86
+ /** 鸿蒙侧没有落盘出口:恒为空串(与「未开启」同口径,调用方无需分支) */
87
+ @When[env == "ohos"]
88
+ public func currentLogFilePath(): String {
89
+ return ""
90
+ }
91
+
92
+ /** 往日志出口追加一行(log 插件用)。公开理由同上 */
93
+ @When[env != "ohos"]
94
+ public func writeLogLine(line: String): Bool {
95
+ var rc: Int32 = -1
96
+ unsafe {
97
+ rc = cj_bridge_log_write(LibC.mallocCString(line))
98
+ }
99
+ return rc == 0
100
+ }
101
+
102
+ /** 鸿蒙侧:没有出口可写,返回 false(调用方据返回值如实回报,不冒充已写入) */
103
+ @When[env == "ohos"]
104
+ public func writeLogLine(line: String): Bool {
105
+ let _ = line
106
+ return false
107
+ }
package/src/logging.cj ADDED
@@ -0,0 +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
+ }