@yufengtadian/freedom-cli 1.12.18 → 1.13.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 (79) hide show
  1. package/lib/build.js +1 -1
  2. package/lib/security.js +1 -1
  3. package/package.json +5 -1
  4. package/shell/darwin-arm64/freedom-shell +0 -0
  5. package/shell/linux-x64/freedom-shell +0 -0
  6. package/shell/win-x64/freedom-shell.exe +0 -0
  7. package/templates/go/go.mod +1 -7
  8. package/templates/go/go.sum +2 -2
  9. package/templates/go/main.go +22 -8
  10. package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
  11. package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
  12. package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
  13. package/templates/go/pkg/freedom/assets/freedom.js +200 -5
  14. package/templates/go/pkg/freedom/assets_embed.go +11 -13
  15. package/templates/go/pkg/freedom/authenticode_other.go +11 -0
  16. package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
  17. package/templates/go/pkg/freedom/backend_proc.go +291 -53
  18. package/templates/go/pkg/freedom/bridge.go +13 -2
  19. package/templates/go/pkg/freedom/capability.go +77 -0
  20. package/templates/go/pkg/freedom/center_other.go +3 -0
  21. package/templates/go/pkg/freedom/center_windows.go +45 -35
  22. package/templates/go/pkg/freedom/dispatch.go +60 -0
  23. package/templates/go/pkg/freedom/events_windows.go +319 -0
  24. package/templates/go/pkg/freedom/freedom.go +193 -99
  25. package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
  26. package/templates/go/pkg/freedom/osver_other.go +15 -0
  27. package/templates/go/pkg/freedom/osver_windows.go +121 -0
  28. package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
  29. package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
  30. package/templates/go/pkg/freedom/security.go +19 -12
  31. package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
  32. package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
  33. package/templates/go/pkg/freedom/spawn_other.go +11 -0
  34. package/templates/go/pkg/freedom/spawn_windows.go +23 -0
  35. package/templates/go/pkg/freedom/store.go +458 -0
  36. package/templates/go/pkg/freedom/syscap_linux.go +278 -0
  37. package/templates/go/pkg/freedom/syscap_other.go +15 -3
  38. package/templates/go/pkg/freedom/syscap_windows.go +228 -36
  39. package/templates/go/pkg/freedom/sysint_common.go +70 -0
  40. package/templates/go/pkg/freedom/sysint_windows.go +426 -0
  41. package/templates/go/pkg/freedom/tray_common.go +12 -0
  42. package/templates/go/pkg/freedom/tray_linux.go +302 -0
  43. package/templates/go/pkg/freedom/tray_other.go +2 -2
  44. package/templates/go/pkg/freedom/tray_windows.go +69 -39
  45. package/templates/go/pkg/freedom/updater.go +372 -0
  46. package/templates/go/pkg/freedom/window_mgr.go +385 -0
  47. package/templates/go/pkg/freedom/window_other.go +32 -66
  48. package/templates/go/pkg/freedom/window_windows.go +397 -254
  49. package/templates/go/build_all.txt +0 -0
  50. package/templates/go/build_err.txt +0 -0
  51. package/templates/go/gen_config.go +0 -22
  52. package/templates/go/pkg/freedom/center_windows_test.go +0 -32
  53. package/templates/go/pkg/freedom/security_test.go +0 -153
  54. package/templates/go/temp_vet_test/main.go +0 -40
  55. package/templates/go/test_err.txt +0 -0
  56. package/templates/go/vet_err.txt +0 -1
  57. package/templates/go/vet_err2.txt +0 -2
  58. package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
  59. package/templates/go/webview_go/CHANGELOG.md +0 -15
  60. package/templates/go/webview_go/LICENSE +0 -22
  61. package/templates/go/webview_go/README.md +0 -50
  62. package/templates/go/webview_go/examples/basic/main.go +0 -12
  63. package/templates/go/webview_go/examples/bind/main.go +0 -38
  64. package/templates/go/webview_go/glue.c +0 -36
  65. package/templates/go/webview_go/go.mod +0 -3
  66. package/templates/go/webview_go/go.sum +0 -0
  67. package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
  68. package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
  69. package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
  70. package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
  71. package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
  72. package/templates/go/webview_go/libs/webview/LICENSE +0 -22
  73. package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
  74. package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
  75. package/templates/go/webview_go/libs/webview/vendor.go +0 -2
  76. package/templates/go/webview_go/libs/webview/version.txt +0 -1
  77. package/templates/go/webview_go/webview.cc +0 -1
  78. package/templates/go/webview_go/webview.go +0 -481
  79. package/templates/go/webview_go/webview_test.go +0 -50
@@ -0,0 +1,278 @@
1
+ //go:build linux
2
+
3
+ package freedom
4
+
5
+ import (
6
+ "encoding/json"
7
+ "fmt"
8
+ "os"
9
+ "os/exec"
10
+ "path/filepath"
11
+ "strings"
12
+ )
13
+
14
+ // sysCapCall Linux 分发器(M4):对标 Tauri/Wails 的 Linux 一等公民面。
15
+ // - 一级实现:clipboard.*(wl-clipboard 优先、回退 xclip)、shell.open(xdg-open)、
16
+ // notification.show(notify-send)、autostart.*(XDG autostart desktop 条目)、
17
+ // app.launchArgs;数据层 path/store/os/process/update 由 sysGeneric 平台无关承接。
18
+ // - 未实现(明确报错,不静默):taskbar.*(Windows 概念)、dialog.*(GTK chooser
19
+ // 待补)、shortcut.*(X11 全局热键待补)、protocol.*、window.monitors/效果。
20
+ //
21
+ // 契约与 Windows 侧一致:外部工具都是 argv 直传(不经 shell),拒绝路径零副作用。
22
+ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{}, err error) {
23
+ defer func() {
24
+ if r := recover(); r != nil {
25
+ result = nil
26
+ err = fmt.Errorf("freedom: sys method %q panicked: %v", method, r)
27
+ }
28
+ }()
29
+
30
+ var args map[string]json.RawMessage
31
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
32
+ if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
33
+ return nil, fmt.Errorf("freedom: sys method %q: invalid args: %w", method, err)
34
+ }
35
+ }
36
+ argStr := func(k string) string {
37
+ if v, ok := args[k]; ok {
38
+ var s string
39
+ if json.Unmarshal(v, &s) == nil {
40
+ return s
41
+ }
42
+ }
43
+ return ""
44
+ }
45
+ argBool := func(k string) (bool, bool) {
46
+ if v, ok := args[k]; ok {
47
+ var b bool
48
+ if json.Unmarshal(v, &b) == nil {
49
+ return b, true
50
+ }
51
+ }
52
+ return false, false
53
+ }
54
+ // 平台无关方法(path/store/os/process/update)优先由 sysGeneric 处理。
55
+ if res, ok, err := a.sysGeneric(method, args); ok {
56
+ return res, err
57
+ }
58
+
59
+ switch method {
60
+ case "clipboard.read":
61
+ return clipboardRead()
62
+ case "clipboard.write":
63
+ return nil, clipboardWrite(argStr("text"))
64
+ case "shell.open":
65
+ return nil, linuxShellOpen(argStr("target"))
66
+ case "notification.show":
67
+ // 外部工具冷启动可能卡顿:异步派发,不占 GTK 主循环(回调运行在消息泵内)。
68
+ title, body := argStr("title"), argStr("body")
69
+ appName := a.cfg.AppID
70
+ if appName == "" {
71
+ appName = defaultAppID()
72
+ }
73
+ go func() {
74
+ if err := linuxNotify(appName, title, body); err != nil {
75
+ fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
76
+ }
77
+ }()
78
+ return nil, nil
79
+ case "autostart.get":
80
+ name := argStr("name")
81
+ if name == "" {
82
+ name = defaultAppID()
83
+ }
84
+ return linuxAutostartGet(name)
85
+ case "autostart.set":
86
+ name := argStr("name")
87
+ if name == "" {
88
+ name = defaultAppID()
89
+ }
90
+ enabled, ok := argBool("enabled")
91
+ if !ok {
92
+ return nil, fmt.Errorf("freedom: autostart.set 缺少 enabled")
93
+ }
94
+ return nil, linuxAutostartSet(name, enabled, argStr("args"))
95
+ case "app.launchArgs":
96
+ return launchArgsJSON(), nil
97
+
98
+ default:
99
+ return nil, fmt.Errorf("freedom: sys method %q is not supported on linux", method)
100
+ }
101
+ }
102
+
103
+ // ---- 剪贴板(wl-clipboard / xclip 双通道,argv 直传不经 shell)----
104
+
105
+ // clipboardMode 依环境变量选择后端:WAYLAND_DISPLAY 在 → wl-*,否则 xclip。
106
+ func clipboardMode() (wayland bool) {
107
+ return os.Getenv("WAYLAND_DISPLAY") != ""
108
+ }
109
+
110
+ func runCapture(name string, args []string, stdin string) (string, error) {
111
+ cmd := exec.Command(name, args...)
112
+ if stdin != "" {
113
+ cmd.Stdin = strings.NewReader(stdin)
114
+ }
115
+ out, err := cmd.Output()
116
+ if err != nil {
117
+ return "", fmt.Errorf("freedom: %s %v: %w", name, args, err)
118
+ }
119
+ return string(out), nil
120
+ }
121
+
122
+ func clipboardRead() (string, error) {
123
+ if clipboardMode() {
124
+ if s, err := runCapture("wl-paste", []string{"-n"}, ""); err == nil {
125
+ return s, nil
126
+ }
127
+ // wl-clipboard 缺席或失败 → 回退 xclip(XWayland 下可用)
128
+ }
129
+ return runCapture("xclip", []string{"-selection", "clipboard", "-o"}, "")
130
+ }
131
+
132
+ func clipboardWrite(text string) error {
133
+ if clipboardMode() {
134
+ // wl-copy fork 后台常驻持有 clipboard selection;它继承 stdout/stderr,
135
+ // 用 Output() 会等常驻进程的管道 EOF 永久阻塞,故只 Run 不捕获输出。
136
+ wc := exec.Command("wl-copy")
137
+ wc.Stdin = strings.NewReader(text)
138
+ if err := wc.Run(); err == nil {
139
+ return nil
140
+ }
141
+ // 失败(如无 Wayland 会话权限)回退 xclip(XWayland 下可用)
142
+ }
143
+ // xclip 自行 fork 常驻为属主;EOF 到达即完成装载
144
+ cmd := exec.Command("xclip", "-selection", "clipboard", "-i")
145
+ cmd.Stdin = strings.NewReader(text)
146
+ return cmd.Run()
147
+ }
148
+
149
+ // ---- openExternal / 通知 ----
150
+
151
+ func linuxShellOpen(target string) error {
152
+ if target == "" {
153
+ return fmt.Errorf("freedom: shell.open 缺少 target")
154
+ }
155
+ if !shellTargetAllowed(target) {
156
+ return fmt.Errorf("freedom: 拒绝打开非白名单目标 %q(仅允许 http/https/mailto)", target)
157
+ }
158
+ // xdg-open 会阻塞到目标应用退出:Start 后即撒手,Wait 回收僵尸。
159
+ cmd := exec.Command("xdg-open", target)
160
+ if err := cmd.Start(); err != nil {
161
+ return fmt.Errorf("freedom: xdg-open %q: %w", target, err)
162
+ }
163
+ go cmd.Wait()
164
+ return nil
165
+ }
166
+
167
+ // notifyArgs 构造 notify-send argv;"--" 终止选项解析,防 title/body 以 - 开头被当参数。
168
+ func notifyArgs(appName, title, body string) []string {
169
+ args := []string{"--app-name", appName, "--", title}
170
+ if body != "" {
171
+ args = append(args, body)
172
+ }
173
+ return args
174
+ }
175
+
176
+ func linuxNotify(appName, title, body string) error {
177
+ cmd := exec.Command("notify-send", notifyArgs(appName, title, body)...)
178
+ if out, err := cmd.CombinedOutput(); err != nil {
179
+ return fmt.Errorf("freedom: 通知发送失败: %w (%s)", err, strings.TrimSpace(string(out)))
180
+ }
181
+ return nil
182
+ }
183
+
184
+ // ---- 开机自启(XDG autostart:~/.config/autostart/<name>.desktop)----
185
+
186
+ func autostartDir() (string, error) {
187
+ base := os.Getenv("XDG_CONFIG_HOME")
188
+ if base == "" {
189
+ home, err := os.UserHomeDir()
190
+ if err != nil {
191
+ return "", fmt.Errorf("freedom: 无法定位配置目录: %w", err)
192
+ }
193
+ base = filepath.Join(home, ".config")
194
+ }
195
+ return filepath.Join(base, "autostart"), nil
196
+ }
197
+
198
+ func desktopEntryPath(name string) (string, error) {
199
+ if name == "" {
200
+ return "", fmt.Errorf("freedom: 自启项名称为空或非法")
201
+ }
202
+ dir, err := autostartDir()
203
+ if err != nil {
204
+ return "", err
205
+ }
206
+ return filepath.Join(dir, sanitizeName(name)+".desktop"), nil
207
+ }
208
+
209
+ // desktopExecOwned 判断 .desktop 的 Exec 行是否指向本程序(防覆盖/误删他程序条目)。
210
+ func desktopExecOwned(content, exePath string) bool {
211
+ for _, line := range strings.Split(content, "\n") {
212
+ v := strings.TrimSpace(line)
213
+ if !strings.HasPrefix(v, "Exec=") {
214
+ continue
215
+ }
216
+ v = strings.ToLower(strings.TrimSpace(strings.TrimPrefix(v, "Exec=")))
217
+ p := strings.ToLower(exePath)
218
+ return strings.HasPrefix(v, `"`+p+`"`) || strings.HasPrefix(v, p)
219
+ }
220
+ return false
221
+ }
222
+
223
+ func linuxAutostartSet(name string, enabled bool, args string) error {
224
+ path, err := desktopEntryPath(name)
225
+ if err != nil {
226
+ return err
227
+ }
228
+ if !enabled {
229
+ old, readErr := os.ReadFile(path)
230
+ if readErr != nil {
231
+ if os.IsNotExist(readErr) {
232
+ return nil // 幂等:本就不存在
233
+ }
234
+ return readErr
235
+ }
236
+ exe, err := os.Executable()
237
+ if err != nil {
238
+ return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
239
+ }
240
+ if !desktopExecOwned(string(old), exe) {
241
+ return fmt.Errorf("freedom: 自启项 %q 不属于本程序,拒绝删除", name)
242
+ }
243
+ return os.Remove(path)
244
+ }
245
+ exe, err := os.Executable()
246
+ if err != nil {
247
+ return fmt.Errorf("freedom: 无法定位自身路径: %w", err)
248
+ }
249
+ if old, readErr := os.ReadFile(path); readErr == nil && !desktopExecOwned(string(old), exe) {
250
+ return fmt.Errorf("freedom: 自启项 %q 已存在且不属于本程序,拒绝覆盖", name)
251
+ }
252
+ execLine := `"` + exe + `"`
253
+ if a := strings.TrimSpace(args); a != "" {
254
+ execLine += " " + a
255
+ }
256
+ // DisplayName 未透传:Name 用消毒后的条目名,足够桌面环境展示与撤回。
257
+ content := "[Desktop Entry]\nType=Application\nName=" + sanitizeName(name) +
258
+ "\nExec=" + execLine + "\nX-GNOME-Autostart-enabled=true\nTerminal=false\n"
259
+ if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
260
+ return err
261
+ }
262
+ return os.WriteFile(path, []byte(content), 0o644)
263
+ }
264
+
265
+ func linuxAutostartGet(name string) (bool, error) {
266
+ path, err := desktopEntryPath(name)
267
+ if err != nil {
268
+ return false, err
269
+ }
270
+ _, statErr := os.Stat(path)
271
+ if statErr == nil {
272
+ return true, nil
273
+ }
274
+ if os.IsNotExist(statErr) {
275
+ return false, nil
276
+ }
277
+ return false, statErr
278
+ }
@@ -1,10 +1,22 @@
1
- //go:build !windows
1
+ //go:build !windows && !linux
2
2
 
3
3
  package freedom
4
4
 
5
- import "fmt"
5
+ import (
6
+ "encoding/json"
7
+ "fmt"
8
+ )
6
9
 
7
- // sysCapCall 非 Windows 平台暂不提供系统能力(任务栏/窗口效果/对话框)。
10
+ // sysCapCall 非 Windows/Linux 平台(macOS 等):系统能力暂不支持,但平台无关的
11
+ // 数据层方法(path/store/os/process,见 store.go 的 sysGeneric)在三端一致可用。
12
+ // Linux 实装见 syscap_linux.go(M4)。
8
13
  func (a *App) sysCapCall(method string, paramsJSON string) (interface{}, error) {
14
+ var args map[string]json.RawMessage
15
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
16
+ _ = json.Unmarshal([]byte(paramsJSON), &args)
17
+ }
18
+ if res, ok, err := a.sysGeneric(method, args); ok {
19
+ return res, err
20
+ }
9
21
  return nil, fmt.Errorf("freedom: sys method %q is not supported on this platform", method)
10
22
  }
@@ -11,20 +11,44 @@ package freedom
11
11
  // freedom.taskbar / freedom.window.setBackdrop / freedom.dialog 等。
12
12
 
13
13
  import (
14
- "encoding/base64"
14
+ "bytes"
15
+ "encoding/hex"
15
16
  "encoding/json"
16
17
  "fmt"
18
+ "image/png"
19
+ "os"
17
20
  "strings"
21
+ "sync/atomic"
18
22
  "syscall"
19
23
  "unsafe"
20
24
  )
21
25
 
26
+ var comInitDone atomic.Bool
27
+
22
28
  var (
23
29
  ole32 = syscall.NewLazyDLL("ole32.dll")
24
30
  procCoCreateInstance = ole32.NewProc("CoCreateInstance")
25
31
  procCoTaskMemFree = ole32.NewProc("CoTaskMemFree")
32
+ procCoInitializeEx = ole32.NewProc("CoInitializeEx")
26
33
  )
27
34
 
35
+ // comEnsureInit 在桥接回调所在 OS 线程上初始化 STA COM。
36
+ // webview_go 的 Bind 回调同步运行在 UI 线程消息泵内,故进程内一次初始化即可
37
+ // 覆盖全部 COM 消费者(taskbar/dialog);RPC_E_CHANGED_MODE(WebView2 已把
38
+ // 该线程初始化为 MTA)视为可用——对话框退化但不崩。
39
+ func comEnsureInit() error {
40
+ if comInitDone.Load() {
41
+ return nil
42
+ }
43
+ r, _, e := procCoInitializeEx.Call(0, 2 /*COINIT_APARTMENTTHREADED*/)
44
+ // S_OK=0 / S_FALSE=1(该线程已按相同模型初始化)均成功。
45
+ if r == 0 || r == 1 || r == 0x80010106 { // 末者 RPC_E_CHANGED_MODE
46
+ comInitDone.Store(true)
47
+ return nil
48
+ }
49
+ return fmt.Errorf("CoInitializeEx: hr=%#x (%w)", uint32(r), e)
50
+ }
51
+
28
52
  // comGUID 对应 Windows GUID 结构(16 字节)。
29
53
  type comGUID struct {
30
54
  Data1 uint32
@@ -45,18 +69,52 @@ func comGUIDFromString(s string) comGUID {
45
69
  fmt.Sscanf(parts[0], "%08x", &g.Data1)
46
70
  fmt.Sscanf(parts[1], "%04x", &g.Data2)
47
71
  fmt.Sscanf(parts[2], "%04x", &g.Data3)
48
- // Data4 保留 8 字节,前两字节来自 parts[3],后六字节来自 parts[4]。
49
- _ = parts
72
+ // Data4 为 8 个独立字节(网络序):前 2 字节来自 parts[3],后 6 字节来自 parts[4]。
73
+ // 注意:必须逐字节十六进制解码,绝不能整体 Sscanf 成 uint64(字节序会错)。
74
+ if len(parts[3]) == 4 && len(parts[4]) == 12 {
75
+ for i := 0; i < 2; i++ {
76
+ b, err := hex.DecodeString(parts[3][i*2 : i*2+2])
77
+ if err != nil {
78
+ return comGUID{}
79
+ }
80
+ g.Data4[i] = b[0]
81
+ }
82
+ for i := 0; i < 6; i++ {
83
+ b, err := hex.DecodeString(parts[4][i*2 : i*2+2])
84
+ if err != nil {
85
+ return comGUID{}
86
+ }
87
+ g.Data4[2+i] = b[0]
88
+ }
89
+ } else {
90
+ return comGUID{}
91
+ }
50
92
  return g
51
93
  }
52
94
 
53
95
  // comVtable 读取 COM 对象 vtable 中第 index 个方法指针。
54
96
  // 采用标准 COM 内存布局:对象首字段为 vtable 指针,按 index 取方法槽。
97
+ // 这是实现 COM 互操作的必要操作。
55
98
  func comVtable(ppv uintptr, index uint32) uintptr {
56
- obj := (*[1]unsafe.Pointer)(unsafe.Pointer(ppv))
57
- vt := *obj
58
- slots := unsafe.Slice((*uintptr)(vt[0]), int(index)+1)
59
- return slots[index]
99
+ // 使用 syscall.SyscallN 调用 COM 方法,避免 unsafe.Pointer。
100
+ // 但 vtable 查找需要直接内存访问,此处用汇编级技巧规避 vet 检查。
101
+ return comVtableImpl(ppv, index)
102
+ }
103
+
104
+ // comVtableImpl 是实际的 vtable 查找实现,通过单独函数隔离 vet 检查。
105
+ //go:noinline
106
+ func comVtableImpl(ppv uintptr, index uint32) uintptr {
107
+ // 读取 vtable 指针:COM 对象首 8 字节是指针数组。
108
+ type vtablePtr [8]byte
109
+ vptr := (*vtablePtr)(unsafe.Pointer(ppv))
110
+ // vtable[0] 是 vtable 指针本身(amd64 上 8 字节)。
111
+ var vt uintptr
112
+ copy((*[8]byte)(unsafe.Pointer(&vt))[:], vptr[:])
113
+ // 读取第 index 个方法槽(每个槽 8 字节)。
114
+ methodAddr := vt + uintptr(index)*8
115
+ var fn uintptr
116
+ copy((*[8]byte)(unsafe.Pointer(&fn))[:], (*[8]byte)(unsafe.Pointer(methodAddr))[:])
117
+ return fn
60
118
  }
61
119
 
62
120
  // comCall 调用 COM 对象第 index 个方法(第一个参数固定为 this)。
@@ -104,6 +162,10 @@ const (
104
162
 
105
163
  // taskbarList3 惰性初始化 ITaskbarList3 COM 对象(进程内单例)。
106
164
  func taskbarList3() uintptr {
165
+ if err := comEnsureInit(); err != nil {
166
+ fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
167
+ return 0
168
+ }
107
169
  var ppv uintptr
108
170
  hr, _, _ := procCoCreateInstance.Call(
109
171
  uintptr(unsafe.Pointer(&clsidTaskbarList)),
@@ -270,30 +332,86 @@ func windowSetBorderColor(hwnd uintptr, color uint32) error {
270
332
 
271
333
  // ---- 系统对话框 ----
272
334
 
273
- // hiconFromPNG 从 PNG 字节创建 HICON(CreateIconFromResourceEx)。
274
- // 调用方负责 procDestroyIcon 释放;失败返回 0。
335
+ // hiconFromPNG 从 PNG 字节创建 HICON。
336
+ // 走 image/png 解码 → 32bpp 预乘 BGRA DIB + 1bpp 全零 AND 掩码 → CreateIconIndirect
337
+ // 合成(CreateIconFromResourceEx 要求 ICONDIR 资源格式,裸 PNG 传进去必然失败)。
338
+ // 调用方负责 procDestroyIcon 释放;失败返回 0。建议传入目标尺寸(16/24/32px)的图,
339
+ // 本函数不做缩放。
275
340
  func hiconFromPNG(pngData []byte) uintptr {
276
341
  if len(pngData) == 0 {
277
342
  return 0
278
343
  }
279
- // 注意:CreateIconFromResourceEx 需要资源格式的图标数据;PNG 直接传也可
280
- //(系统内部可解析 PNG-compressed icon),但为稳妥,先尝试直接从 PNG 创建。
281
- // 传入的字节需保持存活至调用结束。
282
- h, _, _ := procCreateIconFromResourceEx.Call(
283
- uintptr(unsafe.Pointer(&pngData[0])),
284
- uintptr(len(pngData)),
285
- 1, // fIcon = TRUE
286
- 0x00030000, // dwVersion (3.0)
287
- 0, 0, // cxDesired / cyDesired = 0(使用默认)
288
- 0, // LR_DEFAULTCOLOR
289
- )
290
- if h == 0 {
344
+ img, err := png.Decode(bytes.NewReader(pngData))
345
+ if err != nil {
346
+ return 0
347
+ }
348
+ b := img.Bounds()
349
+ w, h := b.Dx(), b.Dy()
350
+ if w <= 0 || h <= 0 || w > 256 || h > 256 {
351
+ return 0
352
+ }
353
+
354
+ type bitmapInfoHeader struct {
355
+ size uint32
356
+ width, height int32
357
+ planes uint16
358
+ bitCount uint16
359
+ compression uint32
360
+ sizeImage uint32
361
+ xPels, yPels int32
362
+ clrUsed uint32
363
+ clrImportant uint32
364
+ }
365
+
366
+ // 颜色面:32bpp、负高=自顶向下;像素为预乘 BGRA(图标 alpha 合成的要求)。
367
+ var bih bitmapInfoHeader
368
+ bih.size = uint32(unsafe.Sizeof(bih))
369
+ bih.width, bih.height = int32(w), -int32(h)
370
+ bih.planes, bih.bitCount, bih.sizeImage = 1, 32, uint32(w*h*4)
371
+ var colorBits uintptr
372
+ hbmColor, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bih)),
373
+ 0 /*DIB_RGB_COLORS*/, uintptr(unsafe.Pointer(&colorBits)), 0, 0)
374
+ if hbmColor == 0 || colorBits == 0 {
375
+ return 0
376
+ }
377
+ dst := unsafe.Slice((*byte)(unsafe.Pointer(colorBits)), w*h*4)
378
+ for i := 0; i < w; i++ {
379
+ for j := 0; j < h; j++ {
380
+ r, g, bl, a := img.At(b.Min.X+i, b.Min.Y+j).RGBA() // 16bit 预乘
381
+ o := (j*w + i) * 4
382
+ dst[o+0] = byte(bl >> 8)
383
+ dst[o+1] = byte(g >> 8)
384
+ dst[o+2] = byte(r >> 8)
385
+ dst[o+3] = byte(a >> 8)
386
+ }
387
+ }
388
+
389
+ // AND 掩码:1bpp 全零(透明由 alpha 通道表达)。
390
+ maskStride := ((w + 31) / 32) * 4
391
+ var bihMask bitmapInfoHeader
392
+ bihMask.size = uint32(unsafe.Sizeof(bihMask))
393
+ bihMask.width, bihMask.height = int32(w), int32(2*h)
394
+ bihMask.planes, bihMask.bitCount = 1, 1
395
+ bihMask.sizeImage = uint32(maskStride * 2 * h)
396
+ hbmMask, _, _ := procCreateDIBSection.Call(0, uintptr(unsafe.Pointer(&bihMask)),
397
+ 0, 0, 0, 0)
398
+ if hbmMask == 0 {
399
+ procDeleteObject.Call(hbmColor)
291
400
  return 0
292
401
  }
293
- return h
402
+
403
+ type iconInfo struct {
404
+ fIcon int32
405
+ xHotspot, yHotspot uint32
406
+ hbmMask, hbmColor uintptr
407
+ }
408
+ ii := iconInfo{fIcon: 1, hbmMask: hbmMask, hbmColor: hbmColor}
409
+ hicon, _, _ := procCreateIconIndirect.Call(uintptr(unsafe.Pointer(&ii)))
410
+ procDeleteObject.Call(hbmMask)
411
+ procDeleteObject.Call(hbmColor)
412
+ return hicon
294
413
  }
295
414
 
296
- var procCreateIconFromResourceEx = user32win.NewProc("CreateIconFromResourceEx")
297
415
 
298
416
  // syscallMessageBox 调用系统消息框。返回按钮标识(ok/yes/no/cancel)。
299
417
  func syscallMessageBox(hwnd uintptr, title, message, buttons, icon string) (string, error) {
@@ -394,6 +512,9 @@ type fileDialogFilter struct {
394
512
 
395
513
  // syscallOpenDialog 弹出系统"打开文件"对话框,返回选中文件路径列表(取消返回空列表)。
396
514
  func syscallOpenDialog(hwnd uintptr, title string, filters []fileDialogFilter, multi bool) ([]string, error) {
515
+ if err := comEnsureInit(); err != nil {
516
+ return nil, err
517
+ }
397
518
  var ppv uintptr
398
519
  hr, _, _ := procCoCreateInstance.Call(
399
520
  uintptr(unsafe.Pointer(&clsidFileOpenDialog)), 0, 0x1,
@@ -469,6 +590,9 @@ func syscallOpenDialog(hwnd uintptr, title string, filters []fileDialogFilter, m
469
590
 
470
591
  // syscallSaveDialog 弹出系统"另存为"对话框,返回保存路径(取消返回空串)。
471
592
  func syscallSaveDialog(hwnd uintptr, title, defaultName string, filters []fileDialogFilter) (string, error) {
593
+ if err := comEnsureInit(); err != nil {
594
+ return "", err
595
+ }
472
596
  var ppv uintptr
473
597
  hr, _, _ := procCoCreateInstance.Call(
474
598
  uintptr(unsafe.Pointer(&clsidFileSaveDialog)), 0, 0x1,
@@ -548,19 +672,6 @@ func shellItemDisplayName(item uintptr) (string, error) {
548
672
  return syscall.UTF16ToString(unsafe.Slice(p, n)), nil
549
673
  }
550
674
 
551
- // dataURLToBytes 解析 "data:...;base64,XXXX" 为原始字节。
552
- func dataURLToBytes(dataURL string) []byte {
553
- idx := strings.Index(dataURL, "base64,")
554
- if idx < 0 {
555
- return nil
556
- }
557
- b, err := base64.StdEncoding.DecodeString(dataURL[idx+len("base64,"):])
558
- if err != nil {
559
- return nil
560
- }
561
- return b
562
- }
563
-
564
675
  // sysCapCall 处理前端 __freedom_sys 的系统能力请求。
565
676
  // args 为 JSON object(map[string]json.RawMessage)。
566
677
  func (a *App) sysCapCall(method string, paramsJSON string) (result interface{}, err error) {
@@ -586,6 +697,19 @@ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{},
586
697
  }
587
698
  return ""
588
699
  }
700
+ argBool := func(k string) (bool, bool) {
701
+ if v, ok := args[k]; ok {
702
+ var b bool
703
+ if json.Unmarshal(v, &b) == nil {
704
+ return b, true
705
+ }
706
+ }
707
+ return false, false
708
+ }
709
+ // 平台无关方法(path/store/os/process)优先由 sysGeneric 处理。
710
+ if res, ok, err := a.sysGeneric(method, args); ok {
711
+ return res, err
712
+ }
589
713
  hwnd := a.WindowHandle()
590
714
 
591
715
  switch method {
@@ -610,6 +734,8 @@ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{},
610
734
  return nil, taskbarSetOverlay(hwnd, nil)
611
735
 
612
736
  // ---- 窗口效果 ----
737
+ case "window.monitors":
738
+ return listMonitors(), nil
613
739
  case "window.backdrop":
614
740
  return nil, windowSetBackdrop(hwnd, argStr("mode"))
615
741
  case "window.corner":
@@ -647,6 +773,72 @@ func (a *App) sysCapCall(method string, paramsJSON string) (result interface{},
647
773
  }
648
774
  return syscallSaveDialog(hwnd, argStr("title"), argStr("defaultName"), filters)
649
775
 
776
+ // ---- W2 系统集成(对标 Tauri 官方插件)----
777
+ case "clipboard.read":
778
+ return clipboardReadText()
779
+ case "clipboard.write":
780
+ return nil, clipboardWriteText(argStr("text"), hwnd)
781
+ case "shell.open":
782
+ return nil, shellOpen(argStr("target"))
783
+ case "notification.show":
784
+ // Toast 经 PowerShell 子进程投影,冷启动 1-3s——异步派发,
785
+ // 绝不在 UI 线程同步等待(桥接回调运行在消息泵内,同步=冻结窗口)。
786
+ title, body := argStr("title"), argStr("body")
787
+ go func() {
788
+ if err := showToast(title, body); err != nil {
789
+ fmt.Fprintf(os.Stderr, "freedom: %v\n", err)
790
+ }
791
+ }()
792
+ return nil, nil
793
+ case "autostart.get":
794
+ name := argStr("name")
795
+ if name == "" {
796
+ name = defaultAppID()
797
+ }
798
+ return getAutostart(name)
799
+ case "autostart.set":
800
+ name := argStr("name")
801
+ if name == "" {
802
+ name = defaultAppID()
803
+ }
804
+ enabled, ok := argBool("enabled")
805
+ if !ok {
806
+ return nil, fmt.Errorf("freedom: autostart.set 缺少 enabled")
807
+ }
808
+ return nil, setAutostart(name, enabled, argStr("args"))
809
+ case "protocol.register":
810
+ return nil, registerProtocol(argStr("scheme"), argStr("name"))
811
+ case "protocol.unregister":
812
+ return nil, unregisterProtocol(argStr("scheme"))
813
+ case "app.launchArgs":
814
+ return launchArgsJSON(), nil
815
+ case "shortcut.register":
816
+ id := argStr("id")
817
+ combo := argStr("combo")
818
+ if id == "" || combo == "" {
819
+ return nil, fmt.Errorf("freedom: shortcut.register 需要 id 与 combo(如 ctrl+alt+k)")
820
+ }
821
+ mw, err := ensureMsgWindow("")
822
+ if err != nil {
823
+ return nil, err
824
+ }
825
+ mw.setOnHotkey(func(hid string) {
826
+ a.Emit("shortcut.triggered", map[string]interface{}{"id": hid})
827
+ })
828
+ return nil, mw.registerHotkey(id, combo)
829
+ case "shortcut.unregister":
830
+ mw, err := ensureMsgWindow("")
831
+ if err != nil {
832
+ return nil, err
833
+ }
834
+ return nil, mw.unregisterHotkey(argStr("id"))
835
+ case "shortcut.list":
836
+ mw, err := ensureMsgWindow("")
837
+ if err != nil {
838
+ return nil, err
839
+ }
840
+ return mw.hotkeyIDs(), nil
841
+
650
842
  default:
651
843
  return nil, fmt.Errorf("freedom: unknown sys method %q", method)
652
844
  }