@yufengtadian/freedom-cli 1.12.12 → 1.12.14

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 (39) hide show
  1. package/README.md +17 -3
  2. package/bin/freedom.js +8 -3
  3. package/lib/build.js +64 -13
  4. package/lib/cli.js +129 -82
  5. package/lib/config.js +0 -7
  6. package/lib/shell.js +170 -33
  7. package/lib/theme.js +105 -0
  8. package/lib/tui.js +27 -3
  9. package/lib/update.js +109 -0
  10. package/lib/utils.js +14 -1
  11. package/package.json +1 -1
  12. package/shell/darwin-arm64/freedom-shell +0 -0
  13. package/shell/linux-x64/freedom-shell +0 -0
  14. package/shell/win-x64/freedom-shell.exe +0 -0
  15. package/templates/go/main.go +2 -1
  16. package/templates/go/pkg/freedom/assets/freedom.js +10 -4
  17. package/templates/go/pkg/freedom/backend_proc.go +7 -4
  18. package/templates/go/pkg/freedom/center_windows.go +44 -5
  19. package/templates/go/pkg/freedom/configfile.go +12 -5
  20. package/templates/go/pkg/freedom/freedom.go +303 -273
  21. package/templates/go/pkg/freedom/window_other.go +6 -2
  22. package/templates/go/webview_go/libs/webview/include/webview.h +31 -0
  23. package/templates/go/webview_go/webview.go +32 -6
  24. package/templates/project/src/main.js +12 -5
  25. package/templates/go/build_out.txt +0 -0
  26. package/templates/go/err.txt +0 -2
  27. package/templates/go/err10.txt +0 -0
  28. package/templates/go/err11.txt +0 -0
  29. package/templates/go/err2.txt +0 -0
  30. package/templates/go/err3.txt +0 -0
  31. package/templates/go/err4.txt +0 -1
  32. package/templates/go/err5.txt +0 -0
  33. package/templates/go/err6.txt +0 -0
  34. package/templates/go/err7.txt +0 -0
  35. package/templates/go/err8.txt +0 -0
  36. package/templates/go/err9.txt +0 -0
  37. package/templates/go/go.mod.bak_gbk +0 -11
  38. package/templates/go/tidy.txt +0 -0
  39. package/templates/go/vet_out.txt +0 -0
@@ -1,273 +1,303 @@
1
- // Package freedom 是一个从零自研的 WebView 桌面壳子框架(对标 Wails / Tauri)。
2
- //
3
- // Freedom 2.0 架构:
4
- // - shell:跨平台渲染壳。复用各系统自带 WebView 内核(Windows WebView2 / macOS
5
- // WKWebView / Linux WebKitGTK),通过跨平台库 webview_go 绑定,自身不携带浏览器
6
- // 内核。壳负责窗口生命周期、前端资源 embed、后端进程管理、IPC 路由。一份壳代码
7
- // 编译三平台。
8
- // - backend:后端抽象。任意时刻绑定一个后端,前端 window.freedom.call 全部路由过去。
9
- // 内置两种实现:
10
- // * EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
11
- // * ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
12
- // 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
13
- // 换一种后端语言无需改壳与前端。
14
- // - ipc:双向桥接。前端 window.freedom.call(method, ...args) -> Promise;
15
- // 后端 app.Emit(event, data) 向所有前端监听器推送事件。
16
- // - assets:前端编译产物(Vite 单文件 HTML)通过 go:embed 嵌入二进制,运行时
17
- // 直接 SetHtml 从内存加载,无需本地 HTTP 端口。
18
- package freedom
19
-
20
- import (
21
- "encoding/json"
22
- "fmt"
23
- "unsafe"
24
-
25
- webview "github.com/webview/webview_go"
26
- )
27
-
28
- // TitleBarMode 描述窗口标题栏策略。
29
- type TitleBarMode string
30
-
31
- const (
32
- // TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
33
- TitleBarNative TitleBarMode = "native"
34
- // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
35
- // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
36
- // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
37
- TitleBarFrameless TitleBarMode = "frameless"
38
- )
39
-
40
- // Config 描述一个 Freedom 应用的窗口与运行配置。
41
- type Config struct {
42
- // Title 是窗口标题。
43
- Title string
44
- // TitleBar 指定标题栏策略(默认 TitleBarNative)。可通过 freedom CLI 一键切换。
45
- TitleBar TitleBarMode
46
- // Width / Height 是窗口初始尺寸(像素)。
47
- Width int
48
- Height int
49
- // Center 为 true 时窗口在屏幕居中(Windows 生效;macOS/Linux 由窗口管理器决定)。
50
- Center bool
51
- // MinWidth / MinHeight 为窗口最小尺寸(<=0 表示不限制)。
52
- MinWidth int
53
- MinHeight int
54
- // Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
55
- Debug bool
56
- // Backend 指定后端适配器。为空时默认使用内嵌 Go 后端(配合 Bind 使用)。
57
- Backend Backend
58
- // HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
59
- // 为 nil 时使用框架内置的默认占位页。
60
- HTML func() (string, error)
61
- }
62
-
63
- // App 是 Freedom 应用实例。
64
- type App struct {
65
- cfg Config
66
- view webview.WebView
67
- backend Backend
68
- backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
69
- onReady func(a *App)
70
- }
71
-
72
- // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
73
- func New(cfg Config) *App {
74
- if cfg.Title == "" {
75
- cfg.Title = "Freedom App"
76
- }
77
- if cfg.TitleBar == "" {
78
- cfg.TitleBar = TitleBarNative
79
- }
80
- if cfg.Width <= 0 {
81
- cfg.Width = 1024
82
- }
83
- if cfg.Height <= 0 {
84
- cfg.Height = 768
85
- }
86
- explicit := cfg.Backend != nil
87
- if !explicit {
88
- cfg.Backend = NewEmbedBackend()
89
- }
90
- return &App{cfg: cfg, backend: cfg.Backend, backendExplicit: explicit}
91
- }
92
-
93
- // OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
94
- // 回调内可以安全调用 Emit 向已注入的页面发送初始化事件。
95
- func (a *App) OnReady(fn func(a *App)) {
96
- a.onReady = fn
97
- }
98
-
99
- // Bind 把后端 Go 方法暴露给前端。仅当后端为内嵌 Go 后端(默认)时有效;
100
- // 进程后端的方法由后端进程自身注册,此处调用会返回错误。
101
- //
102
- // - fn 必须是函数。
103
- // - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
104
- // 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
105
- // - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
106
- func (a *App) Bind(name string, fn interface{}) error {
107
- eb, ok := a.backend.(*EmbedBackend)
108
- if !ok {
109
- return fmt.Errorf("freedom: Bind 仅适用于内嵌 Go 后端;当前后端为 %T,方法请在进程后端中注册", a.backend)
110
- }
111
- return eb.Bind(name, fn)
112
- }
113
-
114
- // Unbind 移除先前 Bind 的方法(内嵌后端)。
115
- func (a *App) Unbind(name string) {
116
- if eb, ok := a.backend.(*EmbedBackend); ok {
117
- eb.Unbind(name)
118
- }
119
- }
120
-
121
- // Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
122
- func (a *App) Run() {
123
- // 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
124
- //(CLI build 时写入;缺失则使用编译期/默认配置)。
125
- _ = a.loadRuntimeConfig()
126
-
127
- html, err := a.resolveHTML()
128
- if err != nil {
129
- fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
130
- return
131
- }
132
-
133
- w := webview.New(a.cfg.Debug)
134
- if w == nil {
135
- fmt.Println("freedom: failed to create webview")
136
- return
137
- }
138
- a.view = w
139
- defer func() {
140
- _ = a.backend.Close()
141
- w.Destroy()
142
- }()
143
-
144
- // 进程后端:启动后端进程并把其推送的事件转发到前端。
145
- if pb, ok := a.backend.(*ProcBackend); ok {
146
- pb.OnEvent(func(event string, data interface{}) {
147
- a.Emit(event, data)
148
- })
149
- if err := pb.start(); err != nil {
150
- fmt.Printf("freedom: backend start failed: %v\n", err)
151
- return
152
- }
153
- }
154
-
155
- w.SetTitle(a.cfg.Title)
156
- w.SetSize(a.cfg.Width, a.cfg.Height, webview.HintNone)
157
- if a.cfg.MinWidth > 0 && a.cfg.MinHeight > 0 {
158
- w.SetSize(a.cfg.MinWidth, a.cfg.MinHeight, webview.HintMin)
159
- }
160
- a.applyCenter()
161
- a.applyTitleBar()
162
- a.setWindowIcon()
163
-
164
- // 注入前端 SDK:window.freedom 全局对象。
165
- w.Init(jsSDK)
166
-
167
- // 绑定 IPC 桥接入口:前端 window.__freedom_bridge(method, paramsJson)。
168
- // webview_go 的 Bind 会让前端调用返回 Promise,Go 侧结果自动 JSON 序列化回传。
169
- if err := w.Bind("__freedom_bridge", a.bridge); err != nil {
170
- fmt.Printf("freedom: failed to bind bridge: %v\n", err)
171
- return
172
- }
173
- // 框架内置方法:健康检查。
174
- if err := w.Bind("__freedom__ping", func() string { return "pong" }); err != nil {
175
- fmt.Printf("freedom: failed to bind ping: %v\n", err)
176
- return
177
- }
178
- // 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用)。
179
- // 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized / isFrameless。
180
- if err := w.Bind("__freedom_window", a.windowControl); err != nil {
181
- fmt.Printf("freedom: failed to bind window control: %v\n", err)
182
- return
183
- }
184
-
185
- if a.onReady != nil {
186
- a.onReady(a)
187
- }
188
-
189
- w.SetHtml(html)
190
- w.Run()
191
- }
192
-
193
- // bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
194
- // 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
195
- // 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
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
-
205
- var params []json.RawMessage
206
- if len(paramsJSON) > 0 && paramsJSON != "null" {
207
- if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
208
- return nil, fmt.Errorf("freedom: method %q: invalid params: %w", method, err)
209
- }
210
- }
211
-
212
- raw, err := a.backend.Handle(method, params)
213
- if err != nil {
214
- return nil, err
215
- }
216
- data, err := json.Marshal(raw)
217
- if err != nil {
218
- return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
219
- }
220
- return json.RawMessage(data), nil
221
- }
222
-
223
- // Emit 把事件推送到前端。前端通过 window.freedom.on(event, cb) 订阅。
224
- // 线程安全:可从任意 goroutine 调用(进程后端推送的事件亦经由本函数)。
225
- func (a *App) Emit(event string, data interface{}) {
226
- if a.view == nil {
227
- return
228
- }
229
- eb, _ := json.Marshal(event)
230
- db, err := json.Marshal(data)
231
- if err != nil {
232
- db = []byte("null")
233
- }
234
- js := "window.freedom && window.freedom.emit(" + string(eb) + "," + string(db) + ");"
235
- a.view.Dispatch(func() {
236
- a.view.Eval(js)
237
- })
238
- }
239
-
240
- // Quit 关闭窗口并退出应用。可从任意 goroutine 调用。
241
- func (a *App) Quit() {
242
- if a.view != nil {
243
- a.view.Dispatch(func() {
244
- a.view.Terminate()
245
- })
246
- }
247
- }
248
-
249
- // WindowHandle 返回底层原生窗口句柄(Windows 上为 HWND)。
250
- func (a *App) WindowHandle() uintptr {
251
- if a.view == nil {
252
- return 0
253
- }
254
- return uintptr(unsafe.Pointer(a.view.Window()))
255
- }
256
-
257
- // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
258
- // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
259
- func (a *App) windowControl(action string) (interface{}, error) {
260
- return windowControl(a, action)
261
- }
262
-
263
- // resolveHTML 依据配置返回页面内容。
264
- // 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
265
- func (a *App) resolveHTML() (string, error) {
266
- if html, err := loadRuntimeHTML(); err == nil && html != "" {
267
- return html, nil
268
- }
269
- if a.cfg.HTML != nil {
270
- return a.cfg.HTML()
271
- }
272
- return defaultHTML, nil
273
- }
1
+ // Package freedom 是一个从零自研的 WebView 桌面壳子框架(对标 Wails / Tauri)。
2
+ //
3
+ // Freedom 2.0 架构:
4
+ // - shell:跨平台渲染壳。复用各系统自带 WebView 内核(Windows WebView2 / macOS
5
+ // WKWebView / Linux WebKitGTK),通过跨平台库 webview_go 绑定,自身不携带浏览器
6
+ // 内核。壳负责窗口生命周期、前端资源 embed、后端进程管理、IPC 路由。一份壳代码
7
+ // 编译三平台。
8
+ // - backend:后端抽象。任意时刻绑定一个后端,前端 window.freedom.call 全部路由过去。
9
+ // 内置两种实现:
10
+ // * EmbedBackend:Go 方法直接注册在壳进程内(app.Bind,兼容 v1 用法)。
11
+ // * ProcBackend:后端是任意语言实现的独立进程(Go/Rust/Python/Node/Java…),
12
+ // 通过 stdin/stdout 上的 NDJSON/JSON-RPC 与壳通信。协议语言无关,
13
+ // 换一种后端语言无需改壳与前端。
14
+ // - ipc:双向桥接。前端 window.freedom.call(method, ...args) -> Promise;
15
+ // 后端 app.Emit(event, data) 向所有前端监听器推送事件。
16
+ // - assets:前端编译产物(Vite 单文件 HTML)通过 go:embed 嵌入二进制,运行时
17
+ // 直接 SetHtml 从内存加载,无需本地 HTTP 端口。
18
+ package freedom
19
+
20
+ import (
21
+ "encoding/json"
22
+ "fmt"
23
+ "sync"
24
+ "unsafe"
25
+
26
+ webview "github.com/webview/webview_go"
27
+ )
28
+
29
+ // TitleBarMode 描述窗口标题栏策略。
30
+ type TitleBarMode string
31
+
32
+ const (
33
+ // TitleBarNative 保留系统原生标题栏(默认),标题栏图标与 exe 图标一致。
34
+ TitleBarNative TitleBarMode = "native"
35
+ // TitleBarFrameless 完全无边框,客户区铺满整个窗口。
36
+ // 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,
37
+ // 需由前端自绘(通过 window.freedom.window.* 控制),三平台行为一致。
38
+ TitleBarFrameless TitleBarMode = "frameless"
39
+ )
40
+
41
+ // Config 描述一个 Freedom 应用的窗口与运行配置。
42
+ type Config struct {
43
+ // Title 是窗口标题。
44
+ Title string
45
+ // TitleBar 指定标题栏策略(默认 TitleBarNative)。可通过 freedom CLI 一键切换。
46
+ TitleBar TitleBarMode
47
+ // Width / Height 是窗口初始尺寸(像素)。
48
+ Width int
49
+ Height int
50
+ // Center 为 true 时窗口在屏幕居中(Windows 生效;macOS/Linux 由窗口管理器决定)。
51
+ Center bool
52
+ // MinWidth / MinHeight 为窗口最小尺寸(<=0 表示不限制)。
53
+ MinWidth int
54
+ MinHeight int
55
+ // Debug 为 true 时开启 WebView 开发者工具(目标平台支持时)。
56
+ Debug bool
57
+ // Backend 指定后端适配器。为空时默认使用内嵌 Go 后端(配合 Bind 使用)。
58
+ Backend Backend
59
+ // HTML 返回要加载到窗口的前端页面内容(内存加载,无本地端口)。
60
+ // 为 nil 时使用框架内置的默认占位页。
61
+ HTML func() (string, error)
62
+ }
63
+
64
+ // App 是 Freedom 应用实例。
65
+ type App struct {
66
+ cfg Config
67
+ viewMu sync.Mutex // 保护 view 字段(Run 写 / Emit·Quit·WindowHandle 任意 goroutine 读)
68
+ view webview.WebView
69
+ backend Backend
70
+ backendExplicit bool // 调用方是否显式指定了后端(外部 config.json 不应覆盖显式绑定)
71
+ onReady func(a *App)
72
+ }
73
+
74
+ // New 创建并初始化一个 Freedom 应用。调用 Run() 之前不会显示窗口。
75
+ func New(cfg Config) *App {
76
+ if cfg.Title == "" {
77
+ cfg.Title = "Freedom App"
78
+ }
79
+ if cfg.TitleBar == "" {
80
+ cfg.TitleBar = TitleBarNative
81
+ }
82
+ if cfg.Width <= 0 {
83
+ cfg.Width = 1024
84
+ }
85
+ if cfg.Height <= 0 {
86
+ cfg.Height = 768
87
+ }
88
+ explicit := cfg.Backend != nil
89
+ if !explicit {
90
+ cfg.Backend = NewEmbedBackend()
91
+ }
92
+ return &App{cfg: cfg, backend: cfg.Backend, backendExplicit: explicit}
93
+ }
94
+
95
+ // OnReady 注册一个回调,在窗口与桥接层就绪、页面加载前执行。
96
+ // 回调内可以安全调用 Emit 向已注入的页面发送初始化事件。
97
+ func (a *App) OnReady(fn func(a *App)) {
98
+ a.onReady = fn
99
+ }
100
+
101
+ // Bind 把后端 Go 方法暴露给前端。仅当后端为内嵌 Go 后端(默认)时有效;
102
+ // 进程后端的方法由后端进程自身注册,此处调用会返回错误。
103
+ //
104
+ // - fn 必须是函数。
105
+ // - 参数与返回值通过 JSON 编解码,前端用 window.freedom.call(name, ...args) 调用,
106
+ // 返回 Promise(resolve 为返回值,reject 为 error 的字符串表示)。
107
+ // - 返回值约定:可返回 (T, error) 或仅 (T) 或仅 error 或空。
108
+ func (a *App) Bind(name string, fn interface{}) error {
109
+ eb, ok := a.backend.(*EmbedBackend)
110
+ if !ok {
111
+ return fmt.Errorf("freedom: Bind 仅适用于内嵌 Go 后端;当前后端为 %T,方法请在进程后端中注册", a.backend)
112
+ }
113
+ return eb.Bind(name, fn)
114
+ }
115
+
116
+ // Unbind 移除先前 Bind 的方法(内嵌后端)。
117
+ func (a *App) Unbind(name string) {
118
+ if eb, ok := a.backend.(*EmbedBackend); ok {
119
+ eb.Unbind(name)
120
+ }
121
+ }
122
+
123
+ // getView / setView 以互斥锁保护 view 字段:Run 在主 goroutine 写,
124
+ // Emit/Quit/WindowHandle 可能被任意 goroutine(如后端事件推送)读,
125
+ // 无保护时 race detector 报数据竞争。
126
+ func (a *App) getView() webview.WebView {
127
+ a.viewMu.Lock()
128
+ defer a.viewMu.Unlock()
129
+ return a.view
130
+ }
131
+
132
+ func (a *App) setView(v webview.WebView) {
133
+ a.viewMu.Lock()
134
+ a.view = v
135
+ a.viewMu.Unlock()
136
+ }
137
+
138
+ // Run 启动窗口并进入主事件循环,阻塞直到窗口被关闭。
139
+ func (a *App) Run() {
140
+ // 通用壳:先加载 exe 同目录 resources/config.json 覆盖窗口与后端配置
141
+ //(CLI build 时写入;缺失则使用编译期/默认配置)。
142
+ // 配置存在但非法时打印告警(不中断启动),避免用户手改配置出错时静默无感。
143
+ if err := a.loadRuntimeConfig(); err != nil {
144
+ fmt.Printf("freedom: warning: %v\n", err)
145
+ }
146
+
147
+ html, err := a.resolveHTML()
148
+ if err != nil {
149
+ fmt.Printf("freedom: failed to resolve HTML: %v\n", err)
150
+ return
151
+ }
152
+
153
+ w := webview.New(a.cfg.Debug)
154
+ if w == nil {
155
+ fmt.Println("freedom: failed to create webview")
156
+ return
157
+ }
158
+ a.setView(w)
159
+ defer func() {
160
+ _ = a.backend.Close()
161
+ w.Destroy()
162
+ }()
163
+
164
+ // 进程后端:启动后端进程并把其推送的事件转发到前端。
165
+ if pb, ok := a.backend.(*ProcBackend); ok {
166
+ pb.OnEvent(func(event string, data interface{}) {
167
+ a.Emit(event, data)
168
+ })
169
+ if err := pb.start(); err != nil {
170
+ fmt.Printf("freedom: backend start failed: %v\n", err)
171
+ return
172
+ }
173
+ }
174
+
175
+ w.SetTitle(a.cfg.Title)
176
+ w.SetSize(a.cfg.Width, a.cfg.Height, webview.HintNone)
177
+ // 最小尺寸:任一项 >0 即生效;未设置的一项传 0 表示该维度不限制
178
+ //(GTK gtk_widget_set_size_request / Cocoa minSize 对 0 均视为"未指定")。
179
+ if a.cfg.MinWidth > 0 || a.cfg.MinHeight > 0 {
180
+ w.SetSize(a.cfg.MinWidth, a.cfg.MinHeight, webview.HintMin)
181
+ }
182
+ a.applyCenter()
183
+ a.applyTitleBar()
184
+ a.setWindowIcon()
185
+
186
+ // 注入前端 SDK:window.freedom 全局对象。
187
+ w.Init(jsSDK)
188
+
189
+ // 绑定 IPC 桥接入口:前端 window.__freedom_bridge(method, paramsJson)。
190
+ // webview_go 的 Bind 会让前端调用返回 Promise,Go 侧结果自动 JSON 序列化回传。
191
+ if err := w.Bind("__freedom_bridge", a.bridge); err != nil {
192
+ fmt.Printf("freedom: failed to bind bridge: %v\n", err)
193
+ return
194
+ }
195
+ // 框架内置方法:健康检查。
196
+ if err := w.Bind("__freedom__ping", func() string { return "pong" }); err != nil {
197
+ fmt.Printf("freedom: failed to bind ping: %v\n", err)
198
+ return
199
+ }
200
+ // 框架内置方法:窗口控制(无边框模式下前端自绘按钮使用)。
201
+ // 支持动作:minimize / maximize / unmaximize / toggleMaximize / close / isMaximized / isFrameless。
202
+ if err := w.Bind("__freedom_window", a.windowControl); err != nil {
203
+ fmt.Printf("freedom: failed to bind window control: %v\n", err)
204
+ return
205
+ }
206
+
207
+ if a.onReady != nil {
208
+ a.onReady(a)
209
+ }
210
+
211
+ w.SetHtml(html)
212
+ w.Run()
213
+ }
214
+
215
+ // bridge 是前端调用后端的统一入口(JSON-RPC 风格)。
216
+ // 前端 SDK 通过 window.__freedom_bridge(method, paramsJson) 调用,
217
+ // 桥接层把请求转发给当前绑定的后端(内嵌 Go 方法或任意语言进程)。
218
+ func (a *App) bridge(method string, paramsJSON string) (result json.RawMessage, err error) {
219
+ // 防御后端方法 panic 导致整个窗口崩溃:统一转为错误回传前端。
220
+ defer func() {
221
+ if r := recover(); r != nil {
222
+ result = nil
223
+ err = fmt.Errorf("freedom: method %q panicked: %v", method, r)
224
+ }
225
+ }()
226
+
227
+ var params []json.RawMessage
228
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
229
+ if err := json.Unmarshal([]byte(paramsJSON), &params); err != nil {
230
+ return nil, fmt.Errorf("freedom: method %q: invalid params: %w", method, err)
231
+ }
232
+ }
233
+
234
+ // 框架内置方法特判:__freedom__ping 由 webview Bind 注册为全局函数
235
+ // window.__freedom__ping(),同时兼容经 freedom.call()/__freedom_bridge 路由的旧写法,
236
+ // 避免模板"测试桥接"自检报"method not bound"。
237
+ if method == "__freedom__ping" {
238
+ return json.RawMessage(`"pong"`), nil
239
+ }
240
+
241
+ raw, err := a.backend.Handle(method, params)
242
+ if err != nil {
243
+ return nil, err
244
+ }
245
+ data, err := json.Marshal(raw)
246
+ if err != nil {
247
+ return nil, fmt.Errorf("freedom: method %q: cannot marshal result: %w", method, err)
248
+ }
249
+ return json.RawMessage(data), nil
250
+ }
251
+
252
+ // Emit 把事件推送到前端。前端通过 window.freedom.on(event, cb) 订阅。
253
+ // 线程安全:可从任意 goroutine 调用(进程后端推送的事件亦经由本函数)。
254
+ func (a *App) Emit(event string, data interface{}) {
255
+ v := a.getView()
256
+ if v == nil {
257
+ return
258
+ }
259
+ eb, _ := json.Marshal(event)
260
+ db, err := json.Marshal(data)
261
+ if err != nil {
262
+ db = []byte("null")
263
+ }
264
+ js := "window.freedom && window.freedom.emit(" + string(eb) + "," + string(db) + ");"
265
+ v.Dispatch(func() {
266
+ v.Eval(js)
267
+ })
268
+ }
269
+
270
+ // Quit 关闭窗口并退出应用。可从任意 goroutine 调用。
271
+ func (a *App) Quit() {
272
+ if v := a.getView(); v != nil {
273
+ v.Dispatch(func() {
274
+ v.Terminate()
275
+ })
276
+ }
277
+ }
278
+
279
+ // WindowHandle 返回底层原生窗口句柄(Windows 上为 HWND)。
280
+ func (a *App) WindowHandle() uintptr {
281
+ if v := a.getView(); v != nil {
282
+ return uintptr(unsafe.Pointer(v.Window()))
283
+ }
284
+ return 0
285
+ }
286
+
287
+ // windowControl 处理前端 window.freedom.window.* 的窗口控制请求。
288
+ // 具体实现按平台分文件:window_windows.go(Windows)/ window_other.go(macOS、Linux)。
289
+ func (a *App) windowControl(action string) (interface{}, error) {
290
+ return windowControl(a, action)
291
+ }
292
+
293
+ // resolveHTML 依据配置返回页面内容。
294
+ // 优先级:exe 同目录 resources/index.html(预编译通用壳,CLI build 写入)> cfg.HTML > 内置占位页。
295
+ func (a *App) resolveHTML() (string, error) {
296
+ if html, err := loadRuntimeHTML(); err == nil && html != "" {
297
+ return html, nil
298
+ }
299
+ if a.cfg.HTML != nil {
300
+ return a.cfg.HTML()
301
+ }
302
+ return defaultHTML, nil
303
+ }
@@ -57,8 +57,12 @@ func windowControl(a *App, action string) (interface{}, error) {
57
57
  // macOS / Linux 暂不提供 exe 内嵌图标提取,前端隐藏标题栏图标。
58
58
  return "", nil
59
59
  case "startDrag":
60
- // macOS / Linux 上无边框窗口通过 WebKit 的 -webkit-app-region: drag 拖动,
61
- // 前端已在 CSS 中声明拖动区域;此处无需原生实现。
60
+ // 无边框窗口拖动:GTK 走 gtk_window_begin_move_drag(root_x/y=-1 用当前指针),
61
+ // Cocoa 走 performWindowDragWithEvent:(用当前鼠标位置合成事件)。
62
+ // 与 Windows 的 WM_NCLBUTTONDOWN+HTCAPTION 语义等价,保留页面双击/右键事件。
63
+ if a.view.BeginMoveDrag() != 0 {
64
+ return nil, fmt.Errorf("window drag is not supported on this platform")
65
+ }
62
66
  return nil, nil
63
67
  default:
64
68
  return nil, fmt.Errorf("unknown window action %q", action)