@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.
- package/lib/build.js +1 -1
- package/lib/security.js +1 -1
- package/package.json +5 -1
- package/shell/darwin-arm64/freedom-shell +0 -0
- package/shell/linux-x64/freedom-shell +0 -0
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/go.mod +1 -7
- package/templates/go/go.sum +2 -2
- package/templates/go/main.go +22 -8
- package/templates/go/pkg/freedom/anti_debug_other.go +2 -3
- package/templates/go/pkg/freedom/anti_debug_windows.go +10 -12
- package/templates/go/pkg/freedom/assets/{index.html → default.html} +1 -1
- package/templates/go/pkg/freedom/assets/freedom.js +200 -5
- package/templates/go/pkg/freedom/assets_embed.go +11 -13
- package/templates/go/pkg/freedom/authenticode_other.go +11 -0
- package/templates/go/pkg/freedom/authenticode_windows.go +87 -0
- package/templates/go/pkg/freedom/backend_proc.go +291 -53
- package/templates/go/pkg/freedom/bridge.go +13 -2
- package/templates/go/pkg/freedom/capability.go +77 -0
- package/templates/go/pkg/freedom/center_other.go +3 -0
- package/templates/go/pkg/freedom/center_windows.go +45 -35
- package/templates/go/pkg/freedom/dispatch.go +60 -0
- package/templates/go/pkg/freedom/events_windows.go +319 -0
- package/templates/go/pkg/freedom/freedom.go +193 -99
- package/templates/go/pkg/freedom/msgwindow_windows.go +257 -0
- package/templates/go/pkg/freedom/osver_other.go +15 -0
- package/templates/go/pkg/freedom/osver_windows.go +121 -0
- package/templates/go/pkg/freedom/proc_hide_windows.go +2 -2
- package/templates/go/pkg/freedom/{configfile.go → resources.go} +38 -35
- package/templates/go/pkg/freedom/security.go +19 -12
- package/templates/go/pkg/freedom/singleinstance_other.go +10 -0
- package/templates/go/pkg/freedom/singleinstance_windows.go +145 -0
- package/templates/go/pkg/freedom/spawn_other.go +11 -0
- package/templates/go/pkg/freedom/spawn_windows.go +23 -0
- package/templates/go/pkg/freedom/store.go +458 -0
- package/templates/go/pkg/freedom/syscap_linux.go +278 -0
- package/templates/go/pkg/freedom/syscap_other.go +15 -3
- package/templates/go/pkg/freedom/syscap_windows.go +228 -36
- package/templates/go/pkg/freedom/sysint_common.go +70 -0
- package/templates/go/pkg/freedom/sysint_windows.go +426 -0
- package/templates/go/pkg/freedom/tray_common.go +12 -0
- package/templates/go/pkg/freedom/tray_linux.go +302 -0
- package/templates/go/pkg/freedom/tray_other.go +2 -2
- package/templates/go/pkg/freedom/tray_windows.go +69 -39
- package/templates/go/pkg/freedom/updater.go +372 -0
- package/templates/go/pkg/freedom/window_mgr.go +385 -0
- package/templates/go/pkg/freedom/window_other.go +32 -66
- package/templates/go/pkg/freedom/window_windows.go +397 -254
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/gen_config.go +0 -22
- package/templates/go/pkg/freedom/center_windows_test.go +0 -32
- package/templates/go/pkg/freedom/security_test.go +0 -153
- package/templates/go/temp_vet_test/main.go +0 -40
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +0 -1
- package/templates/go/vet_err2.txt +0 -2
- package/templates/go/webview_go/.github/workflows/ci.yaml +0 -62
- package/templates/go/webview_go/CHANGELOG.md +0 -15
- package/templates/go/webview_go/LICENSE +0 -22
- package/templates/go/webview_go/README.md +0 -50
- package/templates/go/webview_go/examples/basic/main.go +0 -12
- package/templates/go/webview_go/examples/bind/main.go +0 -38
- package/templates/go/webview_go/glue.c +0 -36
- package/templates/go/webview_go/go.mod +0 -3
- package/templates/go/webview_go/go.sum +0 -0
- package/templates/go/webview_go/libs/mswebview2/LICENSE +0 -27
- package/templates/go/webview_go/libs/mswebview2/include/WebView2.h +0 -23568
- package/templates/go/webview_go/libs/mswebview2/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/vendor.go +0 -2
- package/templates/go/webview_go/libs/mswebview2/version.txt +0 -1
- package/templates/go/webview_go/libs/webview/LICENSE +0 -22
- package/templates/go/webview_go/libs/webview/include/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/include/webview.h +0 -3871
- package/templates/go/webview_go/libs/webview/vendor.go +0 -2
- package/templates/go/webview_go/libs/webview/version.txt +0 -1
- package/templates/go/webview_go/webview.cc +0 -1
- package/templates/go/webview_go/webview.go +0 -481
- 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
|
|
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
|
-
"
|
|
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
|
|
49
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
274
|
-
//
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
-
|
|
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
|
}
|