@yufengtadian/freedom-cli 1.12.1 → 1.12.12
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 +24 -14
- package/lib/cli.js +6 -6
- package/lib/config.js +115 -31
- package/lib/tui.js +1 -1
- package/package.json +1 -1
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/build_out.txt +0 -0
- package/templates/go/err.txt +2 -0
- package/templates/go/err10.txt +0 -0
- package/templates/go/err11.txt +0 -0
- package/templates/go/err2.txt +0 -0
- package/templates/go/err3.txt +0 -0
- package/templates/go/err4.txt +1 -0
- package/templates/go/err5.txt +0 -0
- package/templates/go/err6.txt +0 -0
- package/templates/go/err7.txt +0 -0
- package/templates/go/err8.txt +0 -0
- package/templates/go/err9.txt +0 -0
- package/templates/go/go.mod +6 -0
- package/templates/go/go.mod.bak_gbk +11 -0
- package/templates/go/go.sum +2 -2
- package/templates/go/pkg/freedom/assets/freedom.js +6 -0
- package/templates/go/pkg/freedom/assets/index.html +1 -1
- package/templates/go/pkg/freedom/configfile.go +0 -2
- package/templates/go/pkg/freedom/freedom.go +16 -10
- package/templates/go/pkg/freedom/window_other.go +60 -12
- package/templates/go/pkg/freedom/window_windows.go +244 -29
- package/templates/go/tidy.txt +0 -0
- package/templates/go/vet_out.txt +0 -0
- package/templates/go/webview_go/.github/workflows/ci.yaml +62 -0
- package/templates/go/webview_go/CHANGELOG.md +15 -0
- package/templates/go/webview_go/LICENSE +22 -0
- package/templates/go/webview_go/README.md +50 -0
- package/templates/go/webview_go/examples/basic/main.go +12 -0
- package/templates/go/webview_go/examples/bind/main.go +38 -0
- package/templates/go/webview_go/glue.c +36 -0
- package/templates/go/webview_go/go.mod +3 -0
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +27 -0
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +23568 -0
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/vendor.go +2 -0
- package/templates/go/webview_go/libs/mswebview2/version.txt +1 -0
- package/templates/go/webview_go/libs/webview/LICENSE +22 -0
- package/templates/go/webview_go/libs/webview/include/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/include/webview.h +3840 -0
- package/templates/go/webview_go/libs/webview/vendor.go +2 -0
- package/templates/go/webview_go/libs/webview/version.txt +1 -0
- package/templates/go/webview_go/webview.cc +1 -0
- package/templates/go/webview_go/webview.go +371 -0
- package/templates/go/webview_go/webview_test.go +50 -0
- package/templates/project/freedom.config.js +5 -5
- package/templates/project/index.html +45 -5
- package/templates/project/src/main.js +140 -6
- package/tutorial/tutorial.html +4 -5
package/README.md
CHANGED
|
@@ -1,10 +1,22 @@
|
|
|
1
1
|
# freedom-cli
|
|
2
2
|
|
|
3
|
-
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.
|
|
3
|
+
Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌面应用(v1.13.0)。
|
|
4
4
|
|
|
5
5
|
基于自研 Freedom WebView 壳层(对标 Wails / Tauri):前端完全自由、后端可任意语言、渲染复用系统 WebView(Windows WebView2 / macOS WKWebView / Linux WebKitGTK),产物为单个可执行文件 + resources 目录,前端页面内存加载,不占本地端口。
|
|
6
6
|
|
|
7
|
-
**v1.12.
|
|
7
|
+
**v1.12.12 三平台 frameless 彻底修复**:macOS / Linux 无边框窗口不再残留原生标题栏——
|
|
8
|
+
- 壳层向 webview_go 新增 `set_decorated` / `window_control` 原生窗口控制 API(GTK 走 `gtk_window_set_decorated` / `gtk_window_*`,Cocoa 走隐藏标题栏 + `performMiniaturize:` / `zoom:` / `performClose:` / `isZoomed`),mac/linux 的 frameless 从"空实现回退原生标题栏"修复为真无边框,UI 上方不再残留未清理的原生标题栏;
|
|
9
|
+
- 自绘三按钮(最小化 / 最大化 / 关闭)在 macOS / Linux 上接入原生窗口控制,双击最大化 / 还原、`isMaximized` 状态查询真实可用(此前 mac/linux 窗口控制为静默空转、`isMaximized` 恒 false);
|
|
10
|
+
- 最大化语义统一为"铺满工作区"的正常窗口最大化:Windows 经 `WM_GETMINMAXINFO` 限定到监视器工作区(rcWork),macOS 走 `zoom:`、Linux 走 `gtk_window_maximize`,均非 F11 式整屏全屏;
|
|
11
|
+
- 打包流程全程仍不依赖 Go 工具链(`freedom build` 直接复制预编译壳,详见 v1.1.11 说明)。
|
|
12
|
+
|
|
13
|
+
**v1.12.11 标题栏优化**:自绘标题栏(frameless)增强——
|
|
14
|
+
- 标题栏左侧显示应用图标,图标直接从 exe 内嵌图标(PE 资源 ID=1)提取为 PNG data URL,**不再依赖 resources 资源文件夹**里的任何图标文件,跨平台无 resources 依赖;
|
|
15
|
+
- 双击标题栏空白区最大化 / 还原;
|
|
16
|
+
- 右键标题栏弹出系统菜单(还原 / 最小化 / 最大化 / 关闭),并按窗口状态自动置灰;
|
|
17
|
+
- 标题栏拖动改由 `window.freedom.window.startDrag()` 原生发起,保留双击与右键事件(`-webkit-app-region: drag` 会吞掉页面事件)。
|
|
18
|
+
|
|
19
|
+
**v1.12.0 核心升级**:标题栏默认改为完全无边框(`frameless`)——标题栏不存在,关闭 / 最大化 / 最小化按钮由前端自绘融入 UI(模板已内置自绘标题栏示例),不再依赖 Windows 原生按钮。`freedom titlebar` 仍可随时切回 `native`。
|
|
8
20
|
|
|
9
21
|
**v1.12.x 修复与加固**:
|
|
10
22
|
- 修复 close 按钮缺陷:窗口关闭改走 `PostMessage(WM_CLOSE)`(原 `CloseWindow` 语义为最小化,导致点关闭只最小化);
|
|
@@ -13,7 +25,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
|
|
|
13
25
|
- 壳进程启用 DPI 感知,高 DPI 屏幕下窗口坐标 / 渲染 / 鼠标自动化定位一致,内容更清晰;
|
|
14
26
|
- `freedom config set` 兼容双引号配置写法;依赖变更检测改用 `package-lock.json` 作基准。
|
|
15
27
|
|
|
16
|
-
**v1.11.0 核心升级**:支持自定义应用图标 +
|
|
28
|
+
**v1.11.0 核心升级**:支持自定义应用图标 + 无边框标题栏。配置 `freedom.config.js` 的 `icon`(Windows 用 `.ico`,macOS 用 `.icns`),`freedom build` 时自动把图标注入到 exe 的 PE 资源(RT_ICON / RT_GROUP_ICON)或 `.app/Contents/Resources`,无需任何资源编译器;`freedom icon <path>` 可一键设置。标题栏默认使用无边框(`frameless`),窗口控制按钮由前端自绘,不再占用窗口顶部空间。
|
|
17
29
|
|
|
18
30
|
**v1.10.12 核心升级**:macOS 产物升级为标准 .app 应用包,直接产出 `.app.zip`(mac 用户解压即得 `.app`,拖入 /Applications 即可使用);在 macOS 上执行 `freedom dmg` 可再用系统 hdiutil 生成 `.dmg`。全程仍不依赖 Go 工具链。
|
|
19
31
|
|
|
@@ -21,7 +33,7 @@ Freedom 桌面壳打包工具:把你的 Web 前端一键打包成跨平台桌
|
|
|
21
33
|
|
|
22
34
|
**v1.11.0 新特性**:
|
|
23
35
|
- 自定义应用图标:`icon` 配置 + `freedom icon <path>` 命令,Windows 构建时用 rcedit 注入 `.ico` 到 exe 资源(支持多尺寸,资源管理器 / 任务栏 / 快捷方式统一显示),macOS 构建时把 `.icns` 放入 `.app` 并写入 Info.plist;
|
|
24
|
-
-
|
|
36
|
+
- 标题栏默认无边框(`frameless`):标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,由前端自绘;仍可用 `freedom titlebar` 随时切回 `native`。
|
|
25
37
|
|
|
26
38
|
**v1.10.12 新特性**:
|
|
27
39
|
- macOS 产物打包为标准 `.app` bundle + `.app.zip`:壳复制进 `Contents/MacOS/`,resources 与前端/后端随包,解压即用,无需任何语言运行时;
|
|
@@ -54,7 +66,6 @@ freedom tui
|
|
|
54
66
|
|
|
55
67
|
# 3. 调整标题栏(可选,随时可改;默认 frameless:标题栏不存在,关闭/最大化/最小化按钮由前端自绘融入 UI)
|
|
56
68
|
freedom titlebar native # 保留系统原生标题栏
|
|
57
|
-
freedom titlebar hidden # 隐藏标题栏视觉,仅保留 Windows 原生最小化/最大化/关闭按钮
|
|
58
69
|
freedom titlebar frameless # 完全无边框,标题栏不存在,三按钮由前端自绘(默认,模板已内置示例)
|
|
59
70
|
|
|
60
71
|
# 3.5 设置应用图标(可选;Windows 用 .ico,macOS 用 .icns)
|
|
@@ -64,7 +75,7 @@ freedom icon icon.ico
|
|
|
64
75
|
freedom build # 默认当前平台
|
|
65
76
|
freedom build --platform all # 三平台全量(win + mac + linux)
|
|
66
77
|
freedom build --platform win-x64 # 仅 Windows
|
|
67
|
-
freedom build --platform
|
|
78
|
+
freedom build --platform darwin-arm64 # 仅 macOS Apple Silicon
|
|
68
79
|
freedom build --platform linux-x64 # 仅 Linux
|
|
69
80
|
freedom build --no-cache # 忽略前端构建缓存,强制重建
|
|
70
81
|
```
|
|
@@ -91,7 +102,7 @@ mac 用户解压 `.app.zip` 即得 `.app`,拖入 `/Applications` 即可直接
|
|
|
91
102
|
|
|
92
103
|
**壳二进制平台校验**:`freedom build` 在分发前会读取壳二进制文件头,校验其确为目标平台的真实格式(Windows PE / macOS Mach-O / Linux ELF)。若某平台壳缺失或格式不匹配(例如误用其它平台的二进制顶替),会**明确报错并给出修复指引**,绝不静默产出无法运行的假产物。
|
|
93
104
|
|
|
94
|
-
**预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(
|
|
105
|
+
**预编译壳的来源与 CI**:`webview_go` 依赖各系统自带 WebView 框架,**无法交叉编译**,三平台壳必须在对应平台本机编译。仓库已提供 `.github/workflows/build-shell.yml`:在 win / mac(Apple Silicon)/ linux runner 上分别编译真实壳并上传到 GitHub Release,供 `freedom shell download` 拉取(Intel Mac 已不支持,见 `nativePlatform()` 的明确报错)。发布新版本时:先 `npm publish`,再创建同名 GitHub Release,手动触发 `build-shell` workflow(或直接发 Release 自动触发)即自动补齐各平台壳资产。
|
|
95
106
|
|
|
96
107
|
产物目录可在 `freedom.config.js` 的 `outDir` 中调整:默认 `'dist'`,设为 `'.'` 则直接输出到项目根目录(dist 的上级),设为任意相对 / 绝对路径亦可。输出到项目根目录时会自动跳过 `index.html` 副本,避免覆盖项目源文件。
|
|
97
108
|
|
|
@@ -104,11 +115,10 @@ freedom config set outDir dist # 恢复默认 dist/
|
|
|
104
115
|
|
|
105
116
|
| 模式 | 说明 |
|
|
106
117
|
| --- | --- |
|
|
107
|
-
| `native` |
|
|
108
|
-
| `
|
|
109
|
-
| `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例) |
|
|
118
|
+
| `native` | 保留系统原生标题栏,标题栏图标与 exe 图标一致 |
|
|
119
|
+
| `frameless` | 完全无边框,标题栏不存在,客户区铺满窗口,关闭 / 最大化 / 最小化按钮由前端自绘(默认,模板已内置示例)。当前仅 Windows 完整实现;macOS / Linux 由系统窗口管理器托管,回退为原生标题栏且窗口控制动作返回明确错误 |
|
|
110
120
|
|
|
111
|
-
`
|
|
121
|
+
`frameless` 模式下(Windows),前端可通过注入的 `window.freedom.window` API 控制窗口(`minimize` / `maximize` / `toggleMaximize` / `close` / `isMaximized` / `isFrameless`),模板已内置自绘标题栏示例;macOS / Linux 上这些动作会 reject,前端应捕获并提示。
|
|
112
122
|
|
|
113
123
|
## 配置(freedom.config.js)
|
|
114
124
|
|
|
@@ -121,7 +131,7 @@ export default {
|
|
|
121
131
|
minHeight: 300,
|
|
122
132
|
center: true, // 启动居中
|
|
123
133
|
debug: false, // 开发者工具
|
|
124
|
-
titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native
|
|
134
|
+
titlebar: 'frameless', // frameless(默认,标题栏不存在,三按钮由前端自绘)| native
|
|
125
135
|
icon: undefined, // 应用图标:Windows 用 .ico(推荐多尺寸),macOS 用 .icns
|
|
126
136
|
outDir: 'dist', // 产物目录:'dist'(默认)| '.'(项目根目录)| 任意路径
|
|
127
137
|
// backend: { command: 'node', args: ['backend/main.mjs'] }, // 任意语言后端进程
|
|
@@ -155,8 +165,8 @@ window.freedom.window.minimize(); // 窗口控制
|
|
|
155
165
|
```
|
|
156
166
|
freedom tui # 交互式终端界面
|
|
157
167
|
freedom init <目录> [--force]
|
|
158
|
-
freedom build [--platform win-x64|
|
|
159
|
-
freedom titlebar <native|
|
|
168
|
+
freedom build [--platform win-x64|darwin-arm64|linux-x64|all] [--no-cache]
|
|
169
|
+
freedom titlebar <native|frameless>
|
|
160
170
|
freedom icon <path> # 设置应用图标(Windows 用 .ico,macOS 用 .icns)
|
|
161
171
|
freedom config [get|set]
|
|
162
172
|
freedom shell list|download <platform>|build <platform>
|
package/lib/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ freedom - Freedom 桌面壳打包工具 v${VERSION}
|
|
|
19
19
|
freedom build [--platform <p>] 前端打包并分发桌面应用(默认当前平台)
|
|
20
20
|
--platform win|mac|linux|all 指定目标平台(all = 三平台全量)
|
|
21
21
|
--no-cache 忽略前端构建缓存,强制重新打包
|
|
22
|
-
freedom titlebar <native|
|
|
22
|
+
freedom titlebar <native|frameless>
|
|
23
23
|
一键切换标题栏策略
|
|
24
24
|
freedom icon <path> 设置应用图标(Windows 需 .ico,macOS 需 .icns)
|
|
25
25
|
freedom shell list 列出本地已就绪的预编译壳
|
|
@@ -42,9 +42,9 @@ macOS 产物说明:
|
|
|
42
42
|
在 macOS 上执行 freedom dmg 用系统 hdiutil 生成。
|
|
43
43
|
|
|
44
44
|
标题栏策略说明:
|
|
45
|
-
native
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
native 保留系统原生标题栏,标题栏图标与 exe 图标一致
|
|
46
|
+
frameless 完全无边框,标题栏与 Windows 原生最小化/最大化/关闭按钮均不存在,
|
|
47
|
+
关闭/最大化/最小化按钮由前端自绘(默认,模板已内置示例)
|
|
48
48
|
|
|
49
49
|
图标说明:
|
|
50
50
|
Windows exe 图标:在 freedom.config.js 配置 icon(.ico 路径),构建时自动注入;
|
|
@@ -107,8 +107,8 @@ async function run(argv) {
|
|
|
107
107
|
|
|
108
108
|
case 'titlebar': {
|
|
109
109
|
const mode = rest[0];
|
|
110
|
-
if (!['native', '
|
|
111
|
-
console.error('[freedom] 用法:freedom titlebar <native|
|
|
110
|
+
if (!['native', 'frameless'].includes(mode)) {
|
|
111
|
+
console.error('[freedom] 用法:freedom titlebar <native|frameless>');
|
|
112
112
|
return 1;
|
|
113
113
|
}
|
|
114
114
|
setConfig(process.cwd(), 'titlebar', mode);
|
package/lib/config.js
CHANGED
|
@@ -4,51 +4,114 @@ const fs = require('fs');
|
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const { loadConfig, hasConfig } = require('./utils');
|
|
6
6
|
|
|
7
|
-
//
|
|
8
|
-
const
|
|
9
|
-
name: '
|
|
10
|
-
width:
|
|
11
|
-
height:
|
|
12
|
-
minWidth:
|
|
13
|
-
minHeight:
|
|
14
|
-
center:
|
|
15
|
-
debug:
|
|
16
|
-
titlebar: '
|
|
17
|
-
icon:
|
|
18
|
-
outDir: '
|
|
7
|
+
// 配置文件中可安全写入的字段:类型约束(写入前校验,防止坏值延迟到 build 才爆)。
|
|
8
|
+
const KEY_TYPES = {
|
|
9
|
+
name: 'string',
|
|
10
|
+
width: 'int',
|
|
11
|
+
height: 'int',
|
|
12
|
+
minWidth: 'int',
|
|
13
|
+
minHeight: 'int',
|
|
14
|
+
center: 'bool',
|
|
15
|
+
debug: 'bool',
|
|
16
|
+
titlebar: 'titlebar',
|
|
17
|
+
icon: 'string',
|
|
18
|
+
outDir: 'string',
|
|
19
19
|
};
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// 把 CLI/TUI 传入的值规范化为配置语义上的目标值;非法即抛错。
|
|
22
|
+
function normalizeValue(key, value) {
|
|
23
|
+
const t = KEY_TYPES[key];
|
|
24
|
+
if (t === 'string') return String(value);
|
|
25
|
+
if (t === 'int') {
|
|
26
|
+
const n = Number(value);
|
|
27
|
+
if (!Number.isInteger(n)) {
|
|
28
|
+
throw new Error(`配置项 ${key} 需要整数值,收到:${JSON.stringify(value)}`);
|
|
29
|
+
}
|
|
30
|
+
return n;
|
|
31
|
+
}
|
|
32
|
+
if (t === 'bool') {
|
|
33
|
+
if (typeof value !== 'boolean') {
|
|
34
|
+
throw new Error(`配置项 ${key} 仅接受 true / false`);
|
|
35
|
+
}
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
if (t === 'titlebar') {
|
|
39
|
+
const s = String(value);
|
|
40
|
+
if (!['native', 'frameless'].includes(s)) {
|
|
41
|
+
throw new Error('titlebar 取值必须为:native / frameless。');
|
|
42
|
+
}
|
|
43
|
+
return s;
|
|
44
|
+
}
|
|
45
|
+
// 扩展键(如 backend):字符串命令简写转 {command,args} 对象,
|
|
46
|
+
// 与 build.js renderConfigJSON 的契约对齐(纯字符串形态不会被打包为后端进程)。
|
|
47
|
+
if (key === 'backend') {
|
|
48
|
+
if (value === undefined || value === null || value === '') return undefined;
|
|
49
|
+
if (typeof value === 'string') {
|
|
50
|
+
const parts = value.trim().split(/\s+/);
|
|
51
|
+
return { command: parts[0], args: parts.slice(1) };
|
|
52
|
+
}
|
|
53
|
+
return value; // 对象/数组形式原样写入
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 把值渲染为合法 JS 字面量。统一走 JSON.stringify:
|
|
59
|
+
// 反斜杠 / 引号自动转义,且不存在 $ 替换模式注入面(配合函数式 replace 使用)。
|
|
60
|
+
function renderLiteral(v) {
|
|
61
|
+
if (v === undefined) return 'undefined';
|
|
62
|
+
if (v === null) return 'null';
|
|
63
|
+
return JSON.stringify(v, null, typeof v === 'object' ? 2 : 0);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 统计一行内 { 与 } 的差值,用于把多行对象字面量并入替换范围
|
|
67
|
+
//(启发式实现;配置值内含花括号字符串的场景需手动编辑配置文件)。
|
|
68
|
+
function braceDelta(line) {
|
|
69
|
+
let d = 0;
|
|
70
|
+
for (const ch of line) {
|
|
71
|
+
if (ch === '{') d++;
|
|
72
|
+
else if (ch === '}') d--;
|
|
73
|
+
}
|
|
74
|
+
return d;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 把配置写回 freedom.config.js(保留注释与格式,只替换目标键所在行)。
|
|
78
|
+
// 逐行定位且跳过注释行:模板中被注释的示例键(如 // backend: undefined,)不会被误改。
|
|
22
79
|
function setConfig(dir, key, value) {
|
|
23
80
|
const cfgPath = path.join(dir, 'freedom.config.js');
|
|
24
81
|
if (!fs.existsSync(cfgPath)) {
|
|
25
82
|
throw new Error('未找到 freedom.config.js。');
|
|
26
83
|
}
|
|
27
84
|
const text = fs.readFileSync(cfgPath, 'utf8');
|
|
85
|
+
const norm = normalizeValue(key, value);
|
|
86
|
+
const rendered = renderLiteral(norm);
|
|
28
87
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
88
|
+
const lines = text.split('\n');
|
|
89
|
+
const keyRe = new RegExp(`^(\\s*)${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*:(.*)$`);
|
|
90
|
+
let replaced = false;
|
|
91
|
+
for (let i = 0; i < lines.length; i++) {
|
|
92
|
+
if (lines[i].trim().startsWith('//')) continue; // 注释行不参与匹配与改写
|
|
93
|
+
const m = lines[i].match(keyRe);
|
|
94
|
+
if (!m) continue;
|
|
95
|
+
// 值为多行对象时吞并后续行直到花括号闭合,避免残留孤儿括号
|
|
96
|
+
let j = i;
|
|
97
|
+
let depth = braceDelta(lines[i]);
|
|
98
|
+
while (depth > 0 && j + 1 < lines.length) {
|
|
99
|
+
j++;
|
|
100
|
+
depth += braceDelta(lines[j]);
|
|
33
101
|
}
|
|
102
|
+
const trailing = /,\s*$/.test(m[2]) ? ',' : '';
|
|
103
|
+
lines.splice(i, j - i + 1, `${m[1]}${key}: ${rendered}${trailing}`);
|
|
104
|
+
replaced = true;
|
|
105
|
+
break;
|
|
34
106
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
const regex = new RegExp(`(${key}\\s*:\\s*)(\"[^\"]*\"|'[^']*'|\\d+|true|false|undefined|null)`, 'g');
|
|
38
|
-
if (!regex.test(text)) {
|
|
39
|
-
throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到,请手动添加。`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
let rendered = String(value);
|
|
43
|
-
if (typeof value === 'string') {
|
|
44
|
-
rendered = `'${value.replace(/'/g, "\\'")}'`;
|
|
107
|
+
if (!replaced) {
|
|
108
|
+
throw new Error(`配置项 ${key} 未在 freedom.config.js 中找到(注释行不参与修改),请手动添加。`);
|
|
45
109
|
}
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
fs.writeFileSync(cfgPath, updated, 'utf8');
|
|
49
|
-
return value;
|
|
110
|
+
fs.writeFileSync(cfgPath, lines.join('\n'), 'utf8');
|
|
111
|
+
return norm;
|
|
50
112
|
}
|
|
51
113
|
|
|
114
|
+
|
|
52
115
|
async function getConfig(dir) {
|
|
53
116
|
if (!hasConfig(dir)) {
|
|
54
117
|
throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
|
|
@@ -57,6 +120,27 @@ async function getConfig(dir) {
|
|
|
57
120
|
return cfg;
|
|
58
121
|
}
|
|
59
122
|
|
|
123
|
+
// 配置文件中可安全写入的字段及其默认值(用于渲染或回填)。
|
|
124
|
+
const KNOWN_KEYS = {
|
|
125
|
+
name: 'freedom-app',
|
|
126
|
+
width: 1024,
|
|
127
|
+
height: 720,
|
|
128
|
+
minWidth: 400,
|
|
129
|
+
minHeight: 300,
|
|
130
|
+
center: true,
|
|
131
|
+
debug: false,
|
|
132
|
+
titlebar: 'frameless',
|
|
133
|
+
icon: undefined,
|
|
134
|
+
outDir: 'dist',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
async function getConfig(dir) {
|
|
138
|
+
if (!hasConfig(dir)) {
|
|
139
|
+
throw new Error('当前目录不是 Freedom 项目(缺少 freedom.config.js)。');
|
|
140
|
+
}
|
|
141
|
+
return loadConfig(dir);
|
|
142
|
+
}
|
|
143
|
+
|
|
60
144
|
async function showConfig(dir) {
|
|
61
145
|
const cfg = await loadConfig(dir);
|
|
62
146
|
const lines = Object.keys(KNOWN_KEYS).map((k) => {
|
package/lib/tui.js
CHANGED
|
@@ -192,7 +192,7 @@ const CONFIG_KEYS = [
|
|
|
192
192
|
['minHeight', '窗口最小高度'],
|
|
193
193
|
['center', '启动居中(true/false)'],
|
|
194
194
|
['debug', '开发者工具(true/false)'],
|
|
195
|
-
['titlebar', '标题栏:native |
|
|
195
|
+
['titlebar', '标题栏:native | frameless'],
|
|
196
196
|
['icon', '应用图标:.ico(Windows)/ .icns(macOS)路径'],
|
|
197
197
|
['outDir', '产物目录:dist(默认)| .(项目根)| 任意路径'],
|
|
198
198
|
['backend', '任意语言后端进程(JSON)'],
|
package/package.json
CHANGED
|
Binary file
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
github.com/webview/webview_go: build constraints exclude all Go files in D:\dev\自研desktop exe打包框架\freedom-cli\templates\go\webview_go
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
package/templates/go/go.mod
CHANGED
|
@@ -3,3 +3,9 @@ module freedom-cli-shell
|
|
|
3
3
|
go 1.22
|
|
4
4
|
|
|
5
5
|
require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
|
6
|
+
|
|
7
|
+
require golang.org/x/sys v0.28.0
|
|
8
|
+
|
|
9
|
+
// Freedom fork: 使用随包分发的本地 webview_go(标题栏图标与 exe 图标一致、
|
|
10
|
+
// 无边框窗口最大化限定工作区、彻底清理非客户区等定制),避免拉取上游被覆盖。
|
|
11
|
+
replace github.com/webview/webview_go => ./webview_go
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
module freedom-cli-shell
|
|
2
|
+
|
|
3
|
+
go 1.22
|
|
4
|
+
|
|
5
|
+
require github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6
|
|
6
|
+
|
|
7
|
+
require golang.org/x/sys v0.28.0
|
|
8
|
+
|
|
9
|
+
// Freedom fork: 使用随包分发的本地 webview_go(标题栏图标与 exe 图标一致等定制),
|
|
10
|
+
// 避免拉取上游被覆盖。
|
|
11
|
+
replace github.com/webview/webview_go => ./webview_go
|
package/templates/go/go.sum
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
|
2
|
+
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
|
@@ -52,10 +52,16 @@
|
|
|
52
52
|
minimize: function () { return windowAction('minimize'); },
|
|
53
53
|
maximize: function () { return windowAction('maximize'); },
|
|
54
54
|
unmaximize: function () { return windowAction('unmaximize'); },
|
|
55
|
+
restore: function () { return windowAction('unmaximize'); },
|
|
55
56
|
toggleMaximize: function () { return windowAction('toggleMaximize'); },
|
|
56
57
|
close: function () { return windowAction('close'); },
|
|
57
58
|
isMaximized: function () { return windowAction('isMaximized'); },
|
|
58
59
|
isFrameless: function () { return windowAction('isFrameless'); },
|
|
60
|
+
// 应用图标(data URL)。从 exe 内嵌图标提取,供自绘标题栏显示,不依赖 resources 资源文件夹。
|
|
61
|
+
appIcon: function () { return windowAction('appIcon'); },
|
|
62
|
+
// 标题栏 JS 拖动(WM_NCLBUTTONDOWN + HTCAPTION),比 -webkit-app-region: drag
|
|
63
|
+
// 更稳(保留双击最大化 / 右键菜单等页面事件)。非 Windows 平台为 no-op。
|
|
64
|
+
startDrag: function () { return windowAction('startDrag'); },
|
|
59
65
|
},
|
|
60
66
|
};
|
|
61
67
|
|
|
@@ -29,14 +29,11 @@ import (
|
|
|
29
29
|
type TitleBarMode string
|
|
30
30
|
|
|
31
31
|
const (
|
|
32
|
-
// TitleBarNative
|
|
32
|
+
// TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
|
|
33
33
|
TitleBarNative TitleBarMode = "native"
|
|
34
|
-
// TitleBarHidden 隐藏标题栏视觉,但保留系统原生最小化 / 最大化 / 关闭按钮。
|
|
35
|
-
// 当前仅 Windows 生效(基于 DWM 扩展实现);macOS / Linux 上回退为原生标题栏。
|
|
36
|
-
TitleBarHidden TitleBarMode = "hidden"
|
|
37
34
|
// TitleBarFrameless 完全无边框,客户区铺满整个窗口。
|
|
38
|
-
//
|
|
39
|
-
//
|
|
35
|
+
// 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
|
|
36
|
+
// 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
|
|
40
37
|
TitleBarFrameless TitleBarMode = "frameless"
|
|
41
38
|
)
|
|
42
39
|
|
|
@@ -162,6 +159,7 @@ func (a *App) Run() {
|
|
|
162
159
|
}
|
|
163
160
|
a.applyCenter()
|
|
164
161
|
a.applyTitleBar()
|
|
162
|
+
a.setWindowIcon()
|
|
165
163
|
|
|
166
164
|
// 注入前端 SDK:window.freedom 全局对象。
|
|
167
165
|
w.Init(jsSDK)
|
|
@@ -195,7 +193,15 @@ func (a *App) Run() {
|
|
|
195
193
|
// bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
|
|
196
194
|
// 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
|
|
197
195
|
// 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
|
|
198
|
-
func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error) {
|
|
196
|
+
func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage, err error) {
|
|
197
|
+
// 防御后端方法 panic 导致整个窗口崩溃:统一转为错误回传前端。
|
|
198
|
+
defer func() {
|
|
199
|
+
if r := recover(); r != nil {
|
|
200
|
+
result = nil
|
|
201
|
+
err = fmt.Errorf("freedom: method %q panicked: %v", method, r)
|
|
202
|
+
}
|
|
203
|
+
}()
|
|
204
|
+
|
|
199
205
|
var params []json.RawMessage
|
|
200
206
|
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
201
207
|
if err := json.Unmarshal([]byte(paramsJSON), ¶ms); err != nil {
|
|
@@ -203,11 +209,11 @@ func (a *App) bridge(method string, paramsJSON string) (json.RawMessage, error)
|
|
|
203
209
|
}
|
|
204
210
|
}
|
|
205
211
|
|
|
206
|
-
|
|
212
|
+
raw, err := a.backend.Handle(method, params)
|
|
207
213
|
if err != nil {
|
|
208
214
|
return nil, err
|
|
209
215
|
}
|
|
210
|
-
data, err := json.Marshal(
|
|
216
|
+
data, err := json.Marshal(raw)
|
|
211
217
|
if err != nil {
|
|
212
218
|
return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
|
|
213
219
|
}
|
|
@@ -251,7 +257,7 @@ func (a *App) WindowHandle() uintptr {
|
|
|
251
257
|
// windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
|
|
252
258
|
// 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
|
|
253
259
|
func (a *App) windowControl(action string) (interface{}, error) {
|
|
254
|
-
return windowControl(a
|
|
260
|
+
return windowControl(a, action)
|
|
255
261
|
}
|
|
256
262
|
|
|
257
263
|
// resolveHTML 依据配置返回页面内容。
|
|
@@ -2,24 +2,72 @@
|
|
|
2
2
|
|
|
3
3
|
package freedom
|
|
4
4
|
|
|
5
|
-
import
|
|
5
|
+
import (
|
|
6
|
+
"fmt"
|
|
6
7
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
// 前端仍可通过 window.freedom.window.isFrameless() 感知当前是否无边框。
|
|
10
|
-
func (a *App) applyTitleBar() {}
|
|
8
|
+
webview "github.com/webview/webview_go"
|
|
9
|
+
)
|
|
11
10
|
|
|
12
|
-
//
|
|
13
|
-
|
|
11
|
+
// applyTitleBar 在 macOS / Linux 上通过 webview 层的 set_decorated 控制原生标题栏:
|
|
12
|
+
// - frameless:GTK 走 gtk_window_set_decorated(FALSE),Cocoa 走隐藏标题栏 + 透明标题栏
|
|
13
|
+
// + 移除原生最小化/关闭按钮,彻底去除"UI 上方残留原生标题栏"问题;
|
|
14
|
+
// - native:恢复系统原生标题栏。
|
|
15
|
+
//
|
|
16
|
+
// 窗口在 webview.New() 时已创建,故此处可安全调用(须在 UI 线程,即 Run 流程内)。
|
|
17
|
+
func (a *App) applyTitleBar() {
|
|
18
|
+
if a.view == nil {
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
a.view.SetDecorated(a.cfg.TitleBar != TitleBarFrameless)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// setWindowIcon 在 macOS / Linux 上为空实现(图标由应用包 / 窗口管理器决定)。
|
|
25
|
+
func (a *App) setWindowIcon() {}
|
|
26
|
+
|
|
27
|
+
// windowControl 处理前端 window.freedom.window.* 请求(macOS / Linux 实现)。
|
|
28
|
+
//
|
|
29
|
+
// 窗口控制类动作(最小化/最大化/还原/关闭/查询)直接转发到 webview 层的原生
|
|
30
|
+
// 窗口控制(GTK GtkWindow / Cocoa NSWindow),前端自绘标题栏三按钮因此可用:
|
|
31
|
+
// - GTK: gtk_window_iconify / maximize / unmaximize / close / is_maximized
|
|
32
|
+
// - Cocoa: performMiniaturize: / zoom: / performClose: / isZoomed
|
|
33
|
+
//
|
|
34
|
+
// 注意:webview 层窗口控制动作在 UI 线程执行(前端桥接本身运行于 UI 线程回调中,
|
|
35
|
+
// webview_go 的 binding 回调在主线程派发),无需额外 Dispatch。
|
|
36
|
+
func windowControl(a *App, action string) (interface{}, error) {
|
|
37
|
+
if a.view == nil {
|
|
38
|
+
return nil, fmt.Errorf("window not ready")
|
|
39
|
+
}
|
|
40
|
+
var act webview.WindowAction
|
|
14
41
|
switch action {
|
|
15
|
-
case "
|
|
16
|
-
|
|
42
|
+
case "minimize":
|
|
43
|
+
act = webview.WindowMinimize
|
|
44
|
+
case "maximize":
|
|
45
|
+
act = webview.WindowMaximize
|
|
46
|
+
case "unmaximize", "restore":
|
|
47
|
+
act = webview.WindowRestore
|
|
48
|
+
case "toggleMaximize":
|
|
49
|
+
act = webview.WindowToggleMaximize
|
|
50
|
+
case "close":
|
|
51
|
+
act = webview.WindowClose
|
|
17
52
|
case "isMaximized":
|
|
18
|
-
|
|
19
|
-
case "
|
|
20
|
-
|
|
53
|
+
act = webview.WindowIsMaximized
|
|
54
|
+
case "isFrameless":
|
|
55
|
+
return a.cfg.TitleBar == TitleBarFrameless, nil
|
|
56
|
+
case "appIcon":
|
|
57
|
+
// macOS / Linux 暂不提供 exe 内嵌图标提取,前端隐藏标题栏图标。
|
|
58
|
+
return "", nil
|
|
59
|
+
case "startDrag":
|
|
60
|
+
// macOS / Linux 上无边框窗口通过 WebKit 的 -webkit-app-region: drag 拖动,
|
|
61
|
+
// 前端已在 CSS 中声明拖动区域;此处无需原生实现。
|
|
21
62
|
return nil, nil
|
|
22
63
|
default:
|
|
23
64
|
return nil, fmt.Errorf("unknown window action %q", action)
|
|
24
65
|
}
|
|
66
|
+
if act == webview.WindowIsMaximized {
|
|
67
|
+
return a.view.WindowControl(act) == 1, nil
|
|
68
|
+
}
|
|
69
|
+
if r := a.view.WindowControl(act); r != 0 {
|
|
70
|
+
return nil, fmt.Errorf("window action %q is not supported on this platform", action)
|
|
71
|
+
}
|
|
72
|
+
return nil, nil
|
|
25
73
|
}
|