@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
@@ -3,33 +3,12 @@
3
3
  package freedom
4
4
 
5
5
  import (
6
- "bytes"
7
- "encoding/base64"
8
- "encoding/binary"
6
+ "encoding/json"
9
7
  "fmt"
10
- "image"
11
- "image/color"
12
- "image/png"
13
8
  "syscall"
14
-
15
- "golang.org/x/sys/windows"
16
- "golang.org/x/sys/windows/registry"
9
+ "unsafe"
17
10
  )
18
11
 
19
- // 进程级 DPI 感知:让窗口坐标 / WebView 渲染统一按物理像素工作。
20
- // 若不设置,高 DPI 系统(如 125%/150% 缩放)下 GetWindowRect 返回逻辑坐标,
21
- // 与物理像素的鼠标坐标错位(UI 自动化难定位),且 WebView 内容渲染模糊。
22
- // 必须在窗口创建前生效,故放在 init()。
23
- func init() {
24
- shcore := syscall.NewLazyDLL("shcore.dll")
25
- procSetProcessDpiAwareness := shcore.NewProc("SetProcessDpiAwareness")
26
- r, _, _ := procSetProcessDpiAwareness.Call(2) // PER_MONITOR_DPI_AWARE
27
- if r != 0 {
28
- // 回退到 user32 的旧版 API(Win 8 以下 / 调用失败时)
29
- user32win.NewProc("SetProcessDPIAware").Call()
30
- }
31
- }
32
-
33
12
  // Windows 平台原生窗口操作(user32 / dwmapi)。
34
13
  // 供标题栏策略(applyTitleBar)与前端 window.freedom.window.* 控制使用。
35
14
 
@@ -40,51 +19,127 @@ var (
40
19
  procGetWindowLongPtr = user32win.NewProc("GetWindowLongPtrW")
41
20
  procSetWindowLongPtr = user32win.NewProc("SetWindowLongPtrW")
42
21
  procShowWindow = user32win.NewProc("ShowWindow")
22
+ procPostMessage = user32win.NewProc("PostMessageW")
43
23
  procIsZoomed = user32win.NewProc("IsZoomed")
44
24
  procSetWindowPos = user32win.NewProc("SetWindowPos")
45
25
  procSetWindowText = user32win.NewProc("SetWindowTextW")
46
- procPostMessage = user32win.NewProc("PostMessageW")
47
- procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
48
- procLoadImage = user32win.NewProc("LoadImageW")
49
- procGetSystemMetrics = user32win.NewProc("GetSystemMetrics")
50
- procSendMessage = user32win.NewProc("SendMessageW")
51
- procReleaseCapture = user32win.NewProc("ReleaseCapture")
52
- procGetWindowRect = user32win.NewProc("GetWindowRect")
26
+ // 以下 proc 供 center_windows.go 的 applyCenter 使用(窗口居中),
27
+ // 统一在此声明,避免多文件各自 NewLazyDLL/NewProc 重复加载 user32。
28
+ procGetSystemMetrics = user32win.NewProc("GetSystemMetrics")
53
29
  procMonitorFromWindow = user32win.NewProc("MonitorFromWindow")
54
- procGetMonitorInfo = user32win.NewProc("GetMonitorInfoW")
55
- procMoveWindow = user32win.NewProc("MoveWindow")
56
-
57
- procDestroyIcon = user32win.NewProc("DestroyIcon")
30
+ procGetMonitorInfo = user32win.NewProc("GetMonitorInfoW")
31
+ procMoveWindow = user32win.NewProc("MoveWindow")
32
+ // 以下 proc 供 tray_windows.go / syscap_windows.go 使用(图标加载与释放),
33
+ // 与模板 window_windows.go 保持一致,避免多文件重复声明。
34
+ procGetModuleHandle = kernel32.NewProc("GetModuleHandleW")
35
+ procLoadImage = user32win.NewProc("LoadImageW")
36
+ procDestroyIcon = user32win.NewProc("DestroyIcon")
58
37
 
59
38
  dwmapi = syscall.NewLazyDLL("dwmapi.dll")
60
39
  procDwmExtendFrameIntoArea = dwmapi.NewProc("DwmExtendFrameIntoClientArea")
40
+
41
+ // 以下 proc 供 events_windows.go(窗口事件/全屏)与 windowControl 扩展动作使用。
42
+ comctl32 = syscall.NewLazyDLL("comctl32.dll")
43
+
44
+ procSetWindowSubclass = comctl32.NewProc("SetWindowSubclass")
45
+ procDefSubclassProc = comctl32.NewProc("DefSubclassProc")
46
+
47
+ procGetWindowRect = user32win.NewProc("GetWindowRect")
48
+ procGetClientRect = user32win.NewProc("GetClientRect")
49
+ procBringWindowToTop = user32win.NewProc("BringWindowToTop")
50
+ procIsIconic = user32win.NewProc("IsIconic")
51
+ procIsWindowVisible = user32win.NewProc("IsWindowVisible")
52
+ procGetForegroundWindow = user32win.NewProc("GetForegroundWindow")
53
+ procGetDpiForWindow = user32win.NewProc("GetDpiForWindow")
54
+ procGetWindowTextLength = user32win.NewProc("GetWindowTextLengthW")
55
+ procGetWindowText = user32win.NewProc("GetWindowTextW")
56
+ // 显示器枚举(sysCapCall 的 window.monitors 使用)。
57
+ procEnumDisplayMonitors = user32win.NewProc("EnumDisplayMonitors")
58
+
59
+ // W6 审查修复追加:PNG→HICON 绘制(gdi32)、图标合成、子类化摘除、单实例互斥量。
60
+ gdi32 = syscall.NewLazyDLL("gdi32.dll")
61
+ procCreateDIBSection = gdi32.NewProc("CreateDIBSection")
62
+ procDeleteObject = gdi32.NewProc("DeleteObject")
63
+ procCreateIconIndirect = user32win.NewProc("CreateIconIndirect")
64
+ procRemoveWindowSubclass = comctl32.NewProc("RemoveWindowSubclass")
65
+ procCreateMutexW = kernel32.NewProc("CreateMutexW")
66
+
67
+ shcore = syscall.NewLazyDLL("shcore.dll")
68
+ procGetDpiForMonitor = shcore.NewProc("GetDpiForMonitor")
69
+
70
+ // 以下 proc 供 W2 系统集成(msgwindow/剪贴板/通知/外壳/注册表)使用,
71
+ // 按约定集中在本文件声明,其他文件复用。
72
+ procRegisterHotKey = user32win.NewProc("RegisterHotKey")
73
+ procUnregisterHotKey = user32win.NewProc("UnregisterHotKey")
74
+ procPostQuitMessage = user32win.NewProc("PostQuitMessage")
75
+ procGetMessageW = user32win.NewProc("GetMessageW")
76
+ procTranslateMessage = user32win.NewProc("TranslateMessage")
77
+ procDispatchMessageW = user32win.NewProc("DispatchMessageW")
78
+
79
+ procOpenClipboard = user32win.NewProc("OpenClipboard")
80
+ procCloseClipboard = user32win.NewProc("CloseClipboard")
81
+ procGetClipboardData = user32win.NewProc("GetClipboardData")
82
+ procEmptyClipboard = user32win.NewProc("EmptyClipboard")
83
+ procSetClipboardData = user32win.NewProc("SetClipboardData")
84
+ procIsClipboardFormatAvai = user32win.NewProc("IsClipboardFormatAvailable")
85
+
86
+ // Global* 复用上方 kernel32 声明。
87
+ procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
88
+ procGlobalLock = kernel32.NewProc("GlobalLock")
89
+ procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
90
+ procGlobalSize = kernel32.NewProc("GlobalSize")
91
+ procGlobalFree = kernel32.NewProc("GlobalFree")
92
+
93
+ procFindWindowW = user32win.NewProc("FindWindowW")
94
+ procSendMessageTimeoutW = user32win.NewProc("SendMessageTimeoutW")
95
+
96
+ procShellExecuteW = shell32.NewProc("ShellExecuteW")
97
+ shlwapi = syscall.NewLazyDLL("shlwapi.dll")
98
+ procSHDeleteKeyW = shlwapi.NewProc("SHDeleteKeyW")
99
+
100
+ advapi32 = syscall.NewLazyDLL("advapi32.dll")
101
+ procRegOpenKeyExW = advapi32.NewProc("RegOpenKeyExW")
102
+ procRegCreateKeyExW = advapi32.NewProc("RegCreateKeyExW")
103
+ procRegSetValueExW = advapi32.NewProc("RegSetValueExW")
104
+ procRegQueryValueExW = advapi32.NewProc("RegQueryValueExW")
105
+ procRegDeleteValueW = advapi32.NewProc("RegDeleteValueW")
106
+ procRegCloseKey = advapi32.NewProc("RegCloseKey")
107
+
108
+ procSetProcessDpiAwarenessContext = user32win.NewProc("SetProcessDpiAwarenessContext")
61
109
  )
62
110
 
111
+ // DPI Per-Monitor V2 经进程启动时 API 声明,而非 .syso 内嵌 manifest——
112
+ // WinLibs mingw 链接期会自动注入 default-manifest.o,与自定义 manifest 冲突。
113
+ // init 先于消息循环执行,确保首个窗口创建前生效(对标 Tauri manifest 声明)。
114
+ func init() {
115
+ if procSetProcessDpiAwarenessContext.Find() != nil {
116
+ return // Win10 1703 之前无此 API:保持系统默认感知级
117
+ }
118
+ const dpiPerMonitorV2 = ^uintptr(3) // (DPI_HANDLE)-4:DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2
119
+ procSetProcessDpiAwarenessContext.Call(dpiPerMonitorV2)
120
+ }
121
+
63
122
  const (
64
123
  wsCaption = 0x00C00000 // WS_CAPTION = WS_BORDER | WS_DLGFRAME
65
124
  wsSysMenu = 0x00080000
66
125
  wsThickFrame = 0x00040000
67
126
 
68
- wmClose = 0x0010 // WM_CLOSE:请求窗口正常关闭(触发 GoWnd 的关闭回调)
69
- wmSetIcon = 0x0080 // WM_SETICON:设置窗口大/小图标(ICON_BIG=1 / ICON_SMALL=0)
70
-
71
- rtIcon = 3 // RT_ICON 资源类型(rcedit 注入图标二进制)
72
127
  swHide = 0
73
128
  swShow = 5
74
129
  swMinimize = 6
75
130
  swRestore = 9
76
131
  swMaximize = 3
77
132
 
78
- smCXIcon = 11 // SM_CXICON
79
- smCYIcon = 12 // SM_CYICON
80
- smCXSmall = 13 // SM_CXSMICON
81
- smCYSmall = 14 // SM_CYSMICON
82
-
83
133
  swpFrameChanged = 0x0020
84
134
  swpNoMove = 0x0002
85
135
  swpNoSize = 0x0001
86
136
  swpNoZOrder = 0x0004
87
137
  swpNoActivate = 0x0010
138
+
139
+ wmClose = 0x0010 // WM_CLOSE:请求窗口正常关闭(触发 DestroyWindow 释放 WebView 资源)
140
+
141
+ smCXSmall = 13 // SM_CXSMICON
142
+ smCYSmall = 14 // SM_CYSMICON
88
143
  )
89
144
 
90
145
  // gwlStyle = GWL_STYLE(-16)。用变量声明,避免 uintptr 常量转换溢出。
@@ -110,104 +165,334 @@ func refreshFrame(hwnd uintptr) {
110
165
  }
111
166
 
112
167
  // windowControl 处理前端 window.freedom.window.* 请求(Windows 实现)。
113
- func windowControl(a *App, action string) (interface{}, error) {
114
- hwnd := a.WindowHandle()
168
+ // paramsJSON 为动作参数(JSON object,可空),与 sysCapCall 的 params 风格一致。
169
+ func windowControl(hwnd uintptr, action string, mode TitleBarMode, paramsJSON string) (interface{}, error) {
115
170
  if hwnd == 0 {
116
171
  return nil, fmt.Errorf("window not ready")
117
172
  }
173
+ p := parseWinParams(paramsJSON)
118
174
  switch action {
119
175
  case "minimize":
120
176
  procShowWindow.Call(hwnd, swMinimize)
121
177
  return nil, nil
122
178
  case "maximize":
123
- maximizeWindow(hwnd)
179
+ procShowWindow.Call(hwnd, swMaximize)
124
180
  return nil, nil
125
181
  case "unmaximize", "restore":
126
- restoreWindow(hwnd)
182
+ procShowWindow.Call(hwnd, swRestore)
127
183
  return nil, nil
128
184
  case "toggleMaximize":
129
- if isWindowMaximized(hwnd) {
130
- restoreWindow(hwnd)
185
+ if isZoomed(hwnd) {
186
+ procShowWindow.Call(hwnd, swRestore)
131
187
  } else {
132
- maximizeWindow(hwnd)
188
+ procShowWindow.Call(hwnd, swMaximize)
133
189
  }
134
190
  return nil, nil
135
191
  case "close":
136
- // 发送 WM_CLOSE 请求正常关闭(触发 GoWnd 关闭回调,释放 WebView 资源)。
137
- // 注意:不能使用 user32.CloseWindow —— 该 API 语义是最小化窗口。
192
+ // force=true 时先解除关闭拦截,保证前端确认后能真正关闭。
193
+ if force, _ := p.boolean("force"); force {
194
+ if rt, err := runtimeFor(hwnd); err == nil {
195
+ rt.interceptClose.Store(false)
196
+ }
197
+ }
198
+ // 发送 WM_CLOSE 走正常关闭流程(触发 DestroyWindow,释放 WebView 资源)。
199
+ // 不能使用 user32.CloseWindow——该 API 的语义是最小化窗口而非关闭。
138
200
  procPostMessage.Call(hwnd, wmClose, 0, 0)
139
201
  return nil, nil
140
202
  case "isMaximized":
141
- return isWindowMaximized(hwnd), nil
203
+ return isZoomed(hwnd), nil
142
204
  case "isFrameless":
143
- return a.cfg.TitleBar == TitleBarFrameless, nil
144
- case "appIcon":
145
- // 从 exe 内嵌图标(PE 资源 ID=1)提取,转为 PNG data URL 供前端标题栏显示。
146
- // 不依赖 resources 文件夹里的任何图标文件。
147
- return exeAppIconDataURL(), nil
148
- case "startDrag":
149
- // 前端标题栏 mousedown 时发起窗口拖动(WM_NCLBUTTONDOWN + HTCAPTION)。
150
- // 相比 -webkit-app-region: drag,可保留双击最大化 / 右键菜单等页面事件。
151
- procReleaseCapture.Call()
152
- procSendMessage.Call(hwnd, 0x00A1 /*WM_NCLBUTTONDOWN*/, 2 /*HTCAPTION*/, 0)
205
+ // 仅 frameless 返回 true:hidden 模式保留 DWM 原生按钮,
206
+ // 前端若据 isFrameless 自绘按钮会与原生按钮重叠。
207
+ return mode == TitleBarFrameless, nil
208
+
209
+ // ---- W1 对标 Tauri:位置 / 尺寸 ----
210
+ case "setPosition":
211
+ r, err := getWindowRect(hwnd)
212
+ if err != nil {
213
+ return nil, err
214
+ }
215
+ x, okx := p.intv("x")
216
+ y, oky := p.intv("y")
217
+ if !okx || !oky {
218
+ return nil, fmt.Errorf("setPosition requires x,y")
219
+ }
220
+ procMoveWindow.Call(hwnd, uintptr(int32(x)), uintptr(int32(y)),
221
+ uintptr(r.right-r.left), uintptr(r.bottom-r.top), 1)
222
+ return nil, nil
223
+ case "setSize":
224
+ r, err := getWindowRect(hwnd)
225
+ if err != nil {
226
+ return nil, err
227
+ }
228
+ w, okw := p.intv("width")
229
+ h, okh := p.intv("height")
230
+ if !okw || !okh {
231
+ return nil, fmt.Errorf("setSize requires width,height")
232
+ }
233
+ procMoveWindow.Call(hwnd, uintptr(r.left), uintptr(r.top), uintptr(int32(w)), uintptr(int32(h)), 1)
234
+ return nil, nil
235
+ case "getPosition":
236
+ r, err := getWindowRect(hwnd)
237
+ if err != nil {
238
+ return nil, err
239
+ }
240
+ return map[string]int{"x": int(r.left), "y": int(r.top)}, nil
241
+ case "getSize":
242
+ r, err := getWindowRect(hwnd)
243
+ if err != nil {
244
+ return nil, err
245
+ }
246
+ return map[string]int{"width": int(r.right - r.left), "height": int(r.bottom - r.top)}, nil
247
+ case "innerSize":
248
+ r := getClientRect(hwnd)
249
+ return map[string]int{"width": int(r.right - r.left), "height": int(r.bottom - r.top)}, nil
250
+ case "center":
251
+ r, err := getWindowRect(hwnd)
252
+ if err != nil {
253
+ return nil, err
254
+ }
255
+ x, y := centeredForWindow(hwnd, int(r.right-r.left), int(r.bottom-r.top))
256
+ procMoveWindow.Call(hwnd, uintptr(int32(x)), uintptr(int32(y)),
257
+ uintptr(r.right-r.left), uintptr(r.bottom-r.top), 1)
258
+ return nil, nil
259
+ case "setTitle":
260
+ s, ok := p.strv("title")
261
+ if !ok {
262
+ return nil, fmt.Errorf("setTitle requires title")
263
+ }
264
+ ptr, err := syscall.UTF16PtrFromString(s)
265
+ if err != nil {
266
+ return nil, fmt.Errorf("setTitle: title must not contain NUL")
267
+ }
268
+ procSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(ptr)))
269
+ return nil, nil
270
+
271
+ // ---- W1:可见性 / 层级 / 焦点 ----
272
+ case "show":
273
+ procShowWindow.Call(hwnd, swShow)
274
+ return nil, nil
275
+ case "hide":
276
+ procShowWindow.Call(hwnd, swHide)
277
+ return nil, nil
278
+ case "focus":
279
+ if isIconic(hwnd) {
280
+ procShowWindow.Call(hwnd, swRestore)
281
+ }
282
+ procBringWindowToTop.Call(hwnd)
283
+ procSetForegroundWindow.Call(hwnd)
284
+ return nil, nil
285
+ case "isVisible":
286
+ r, _, _ := procIsWindowVisible.Call(hwnd)
287
+ return r != 0, nil
288
+ case "isFocused":
289
+ fg, _, _ := procGetForegroundWindow.Call()
290
+ return fg == hwnd, nil
291
+ case "isMinimized":
292
+ return isIconic(hwnd), nil
293
+ case "setAlwaysOnTop":
294
+ on, ok := p.boolean("on")
295
+ if !ok {
296
+ return nil, fmt.Errorf("setAlwaysOnTop requires on")
297
+ }
298
+ top := hwndNotopmost
299
+ if on {
300
+ top = hwndTopmost
301
+ }
302
+ procSetWindowPos.Call(hwnd, top, 0, 0, 0, 0, swpNoMove|swpNoSize|swpNoActivate)
303
+ return nil, nil
304
+ case "setSkipTaskbar":
305
+ on, ok := p.boolean("on")
306
+ if !ok {
307
+ return nil, fmt.Errorf("setSkipTaskbar requires on")
308
+ }
309
+ ex, _, _ := procGetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)))
310
+ if on {
311
+ ex |= wsExToolwindow
312
+ } else {
313
+ ex &^= wsExToolwindow
314
+ }
315
+ procSetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)), ex)
316
+ // 任务栏按钮变化需 SWP_FRAMECHANGED + 显示标志才能即时生效。
317
+ procSetWindowPos.Call(hwnd, 0, 0, 0, 0, 0,
318
+ swpFrameChanged|swpNoMove|swpNoSize|swpNoZOrder|swpNoActivate|swpShowWindow)
319
+ return nil, nil
320
+
321
+ // ---- W1:行为开关 ----
322
+ case "setResizable":
323
+ on, ok := p.boolean("on")
324
+ if !ok {
325
+ return nil, fmt.Errorf("setResizable requires on")
326
+ }
327
+ style := getWindowStyle(hwnd)
328
+ if on {
329
+ style |= wsThickFrame
330
+ } else {
331
+ style &^= wsThickFrame
332
+ }
333
+ setWindowStyle(hwnd, style)
334
+ refreshFrame(hwnd)
335
+ return nil, nil
336
+ case "setMaximizable":
337
+ on, ok := p.boolean("on")
338
+ if !ok {
339
+ return nil, fmt.Errorf("setMaximizable requires on")
340
+ }
341
+ style := getWindowStyle(hwnd)
342
+ if on {
343
+ style |= wsMaximizeBox
344
+ } else {
345
+ style &^= wsMaximizeBox
346
+ }
347
+ setWindowStyle(hwnd, style)
348
+ refreshFrame(hwnd)
349
+ return nil, nil
350
+ case "setMinimizable":
351
+ on, ok := p.boolean("on")
352
+ if !ok {
353
+ return nil, fmt.Errorf("setMinimizable requires on")
354
+ }
355
+ style := getWindowStyle(hwnd)
356
+ if on {
357
+ style |= wsMinimizeBox
358
+ } else {
359
+ style &^= wsMinimizeBox
360
+ }
361
+ setWindowStyle(hwnd, style)
362
+ refreshFrame(hwnd)
153
363
  return nil, nil
364
+
365
+ // ---- W1:全屏 / 关闭拦截 ----
366
+ case "setFullscreen":
367
+ on, ok := p.boolean("on")
368
+ if !ok {
369
+ return nil, fmt.Errorf("setFullscreen requires on")
370
+ }
371
+ return nil, setFullscreen(hwnd, on)
372
+ case "isFullscreen":
373
+ rt, err := runtimeFor(hwnd)
374
+ if err != nil {
375
+ return false, nil
376
+ }
377
+ return rt.getFullscreen(), nil
378
+ case "interceptClose":
379
+ on, ok := p.boolean("on")
380
+ if !ok {
381
+ return nil, fmt.Errorf("interceptClose requires on")
382
+ }
383
+ rt, err := runtimeFor(hwnd)
384
+ if err != nil {
385
+ return nil, err
386
+ }
387
+ rt.interceptClose.Store(on)
388
+ return nil, nil
389
+
390
+ // ---- W1:信息查询 ----
391
+ case "getInfo":
392
+ return getWindowInfo(hwnd, mode)
154
393
  default:
155
394
  return nil, fmt.Errorf("unknown window action %q", action)
156
395
  }
157
396
  }
158
397
 
159
- // maximizeWindow 最大化窗口。
160
- // 直接走系统 SW_MAXIMIZE:壳层(webview_go fork)在 WndProc 的 WM_GETMINMAXINFO 中
161
- // 已把无边框窗口的最大化边界限定到监视器工作区(rcWork),
162
- // 因此"最大化 = 铺满任务栏上方的工作区",而非 F11 式整屏全屏。
163
- func maximizeWindow(hwnd uintptr) {
164
- procShowWindow.Call(hwnd, swMaximize)
398
+ // getWindowInfo 聚合窗口状态(物理像素;scaleFactor 为 DPI 缩放)。
399
+ func getWindowInfo(hwnd uintptr, mode TitleBarMode) (map[string]interface{}, error) {
400
+ r, err := getWindowRect(hwnd)
401
+ if err != nil {
402
+ return nil, err
403
+ }
404
+ cr := getClientRect(hwnd)
405
+ ex, _, _ := procGetWindowLongPtr.Call(hwnd, uintptr(int(gwlExStyle)))
406
+ vis, _, _ := procIsWindowVisible.Call(hwnd)
407
+ fg, _, _ := procGetForegroundWindow.Call()
408
+ rt, rtErr := runtimeFor(hwnd)
409
+ return map[string]interface{}{
410
+ "title": getWindowText(hwnd),
411
+ "outerX": int(r.left),
412
+ "outerY": int(r.top),
413
+ "outerWidth": int(r.right - r.left),
414
+ "outerHeight": int(r.bottom - r.top),
415
+ "innerWidth": int(cr.right - cr.left),
416
+ "innerHeight": int(cr.bottom - cr.top),
417
+ "scaleFactor": windowScaleFactor(hwnd),
418
+ "visible": vis != 0,
419
+ "focused": fg == hwnd,
420
+ "maximized": isZoomed(hwnd),
421
+ "minimized": isIconic(hwnd),
422
+ "alwaysOnTop": ex&wsExTopmost != 0,
423
+ "skipTaskbar": ex&wsExToolwindow != 0,
424
+ "frameless": mode == TitleBarFrameless,
425
+ "fullscreen": rtErr == nil && rt.getFullscreen(),
426
+ }, nil
165
427
  }
166
428
 
167
- // restoreWindow 还原窗口(系统 SW_RESTORE,恢复到最大化前的位置与尺寸)。
168
- func restoreWindow(hwnd uintptr) {
169
- procShowWindow.Call(hwnd, swRestore)
429
+ func isIconic(hwnd uintptr) bool {
430
+ r, _, _ := procIsIconic.Call(hwnd)
431
+ return r != 0
170
432
  }
171
433
 
172
- // isWindowMaximized 判断窗口当前是否处于最大化状态。
173
- func isWindowMaximized(hwnd uintptr) bool {
174
- return isZoomed(hwnd)
434
+ // getWindowText 读取窗口标题(GetWindowTextW)。
435
+ func getWindowText(hwnd uintptr) string {
436
+ n, _, _ := procGetWindowTextLength.Call(hwnd)
437
+ if n == 0 {
438
+ return ""
439
+ }
440
+ buf := make([]uint16, n+1)
441
+ procGetWindowText.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), uintptr(len(buf)))
442
+ return syscall.UTF16ToString(buf)
175
443
  }
176
444
 
177
- // exeIconData 读取当前 exe 内嵌的 RT_ICON 资源原始字节(rcedit 注入时资源 ID=1)。
178
- // 不依赖 RT_GROUP_ICON 的 ID(rcedit 固定写入 0,而 LoadImageW 按硬编码 ID 加载会失败),
179
- // 直接取 RT_ICON 二进制,兼容任意注入工具。
180
- // 使用 x/sys/windows 类型化 API(LoadResourceData 直接返回字节副本,无需 unsafe 指针转换)。
181
- func exeIconData() []byte {
182
- var hInst windows.Handle
183
- if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
184
- return nil
445
+ // winParams 是窗口动作的 JSON 参数对象。
446
+ type winParams map[string]json.RawMessage
447
+
448
+ func parseWinParams(paramsJSON string) winParams {
449
+ var p winParams
450
+ if len(paramsJSON) > 0 && paramsJSON != "null" {
451
+ _ = json.Unmarshal([]byte(paramsJSON), &p)
185
452
  }
186
- // 先按 rcedit 注入约定 RT_ICON ID=1 读取;失败回退 ID=0(M3)。
187
- // 不同注入工具写入的资源 ID 可能不同,硬编码单一 ID 会导致"标题栏图标正常
188
- // 但 appIcon 空白"的中间态,此处与 loadExeIcon 的 ID 假设解耦。
189
- for _, id := range []uintptr{1, 0} {
190
- // FindResourceW(hModule, MAKEINTRESOURCE(id), MAKEINTRESOURCE(RT_ICON))
191
- resInfo, err := windows.FindResource(hInst, windows.ResourceID(id), windows.ResourceID(rtIcon))
192
- if err != nil {
193
- continue
453
+ return p
454
+ }
455
+
456
+ func (p winParams) intv(k string) (int, bool) {
457
+ if v, ok := p[k]; ok {
458
+ var f float64
459
+ if json.Unmarshal(v, &f) == nil {
460
+ return int(f), true
194
461
  }
195
- if data, err := windows.LoadResourceData(hInst, resInfo); err == nil {
196
- return data
462
+ }
463
+ return 0, false
464
+ }
465
+
466
+ func (p winParams) strv(k string) (string, bool) {
467
+ if v, ok := p[k]; ok {
468
+ var s string
469
+ if json.Unmarshal(v, &s) == nil {
470
+ return s, true
471
+ }
472
+ }
473
+ return "", false
474
+ }
475
+
476
+ func (p winParams) boolean(k string) (bool, bool) {
477
+ if v, ok := p[k]; ok {
478
+ var b bool
479
+ if json.Unmarshal(v, &b) == nil {
480
+ return b, true
197
481
  }
198
482
  }
199
- return nil
483
+ return false, false
200
484
  }
201
485
 
202
- // isPNG 判断字节流是否为 PNG 编码(rcedit 注入的 .ico 内嵌 PNG 时 RT_ICON 数据即 PNG)。
203
- func isPNG(b []byte) bool {
204
- return len(b) >= 8 && b[0] == 0x89 && b[1] == 'P' && b[2] == 'N' && b[3] == 'G'
486
+ func isZoomed(hwnd uintptr) bool {
487
+ r, _, _ := procIsZoomed.Call(hwnd)
488
+ return r != 0
205
489
  }
206
490
 
207
491
  // loadExeIcon 从 exe 内嵌的 RT_GROUP_ICON 资源加载 HICON。
208
492
  // rcedit 注入的 group icon 资源 ID 固定为 0(LoadImageW 需按该 ID 加载;
209
493
  // 历史按 ID=1 硬编码导致加载失败,故此处显式用 0)。
210
494
  // 调用方用完须 procDestroyIcon 释放;加载失败返回 0。
495
+ // 供 tray_windows.go 托盘图标与 syscap_windows.go 窗口效果使用。
211
496
  func loadExeIcon(cx, cy uintptr) uintptr {
212
497
  hInst, _, _ := procGetModuleHandle.Call(0)
213
498
  if hInst == 0 {
@@ -224,49 +509,6 @@ func loadExeIcon(cx, cy uintptr) uintptr {
224
509
  return 0
225
510
  }
226
511
 
227
- // exeAppIconDataURL 从当前 exe 的 PE 资源图标(rcedit 注入的 RT_ICON)提取为 PNG data URL。
228
- // 无法提取时返回空字符串(前端隐藏图标)。
229
- func exeAppIconDataURL() string {
230
- data := exeIconData()
231
- if len(data) == 0 {
232
- return ""
233
- }
234
- // rcedit 注入 .ico 内嵌 PNG 时,RT_ICON 数据即 PNG 字节,直接编码
235
- if isPNG(data) {
236
- return "data:image/png;base64," + base64.StdEncoding.EncodeToString(data)
237
- }
238
- // 非 PNG(BMP/DIB):纯 Go 解析像素并编码为 PNG,无需 GDI 绘制
239
- img, err := dibToPNG(data)
240
- if err != nil {
241
- return ""
242
- }
243
- return "data:image/png;base64," + base64.StdEncoding.EncodeToString(img)
244
- }
245
-
246
- // webview2Available 检测 Windows 是否已安装 WebView2 Runtime。
247
- // webview_create(WEBVIEW_EDGE)失败的最常见原因是缺少 WebView2 Runtime,
248
- // 检测其安装注册表键(EdgeUpdate Clients GUID)判断状态,便于给出可操作提示。
249
- // 返回值仅用于决定错误文案,不参与窗口创建逻辑。
250
- func webview2Available() bool {
251
- guid := `{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}`
252
- key := `SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\` + guid
253
- if k, err := registry.OpenKey(registry.LOCAL_MACHINE, key, registry.QUERY_VALUE|registry.READ); err == nil {
254
- k.Close()
255
- return true
256
- }
257
- key2 := `SOFTWARE\Microsoft\EdgeUpdate\Clients\` + guid
258
- if k, err := registry.OpenKey(registry.LOCAL_MACHINE, key2, registry.QUERY_VALUE|registry.READ); err == nil {
259
- k.Close()
260
- return true
261
- }
262
- return false
263
- }
264
-
265
- func isZoomed(hwnd uintptr) bool {
266
- r, _, _ := procIsZoomed.Call(hwnd)
267
- return r != 0
268
- }
269
-
270
512
  // applyTitleBar 依据配置调整窗口标题栏(Windows 实现)。
271
513
  func (a *App) applyTitleBar() {
272
514
  hwnd := a.WindowHandle()
@@ -275,123 +517,24 @@ func (a *App) applyTitleBar() {
275
517
  }
276
518
  switch a.cfg.TitleBar {
277
519
  case TitleBarFrameless:
278
- // 完全无边框:去掉标题栏 / 系统菜单 / 可调边框(WS_THICKFRAME)。
279
- // - 标题栏与系统原生最小化 / 最大化 / 关闭按钮均不存在,由前端自绘
280
- // (window.freedom.window.*)接管;
281
- // - 去掉 WS_THICKFRAME 是最大化行为正确的关键:只要该位存在,系统在
282
- // WM_GETMINMAXINFO 之后仍会按 AdjustWindowRectEx 追加标题栏 + 边框空间,
283
- // 导致最大化窗口溢出工作区、盖住任务栏(F11 全屏感);
284
- // - 边缘拖拽 resize 由壳层 WndProc 的 WM_NCHITTEST 手动实现(返回 HT* 边界值),
285
- // 不依赖 WS_THICKFRAME。
286
- style := getWindowStyle(hwnd)
287
- style &^= wsCaption | wsSysMenu | wsThickFrame
288
- setWindowStyle(hwnd, style)
289
- refreshFrame(hwnd)
290
- default: // TitleBarNative:补齐系统原生标题栏与可调边框。
291
- // webview_go 底层创建的窗口默认 style 不含 WS_CAPTION(见 WebView2 窗口创建),
292
- // 因此 native 模式必须显式补回标题栏 / 系统菜单 / 可调边框,
293
- // 否则窗口会表现为无边框(无原生三按钮、不可拖拽调整大小)。
520
+ // 完全无边框:去掉标题栏 / 系统菜单,客户区铺满整个窗口。
521
+ // 最小化 / 最大化 / 关闭按钮由前端自绘(window.freedom.window.*)。
294
522
  style := getWindowStyle(hwnd)
295
- style |= wsCaption | wsSysMenu | wsThickFrame
523
+ style &^= wsCaption | wsSysMenu
296
524
  setWindowStyle(hwnd, style)
297
525
  refreshFrame(hwnd)
298
- }
299
- }
300
-
301
- // setWindowIcon 把 exe 内嵌的应用程序图标同步到窗口标题栏与任务栏:
302
- // - native 模式:标题栏图标(ICON_SMALL)与任务栏 / Alt-Tab 图标(ICON_BIG)均来自 exe 图标,
303
- // 保证「标题栏图标 = exe 图标」;
304
- // - frameless 模式:无标题栏,但任务栏 / Alt-Tab 仍显示 exe 图标。
305
- //
306
- // 图标经 LoadImageW 从 PE 资源 RT_GROUP_ICON(ID=0) 加载(rcedit 注入的 group icon ID=0),
307
- // 不依赖 resources 文件夹里的任何图标文件。
308
- func (a *App) setWindowIcon() {
309
- hwnd := a.WindowHandle()
310
- if hwnd == 0 {
311
- return
312
- }
313
- cxIcon, _, _ := procGetSystemMetrics.Call(smCXIcon)
314
- cyIcon, _, _ := procGetSystemMetrics.Call(smCYIcon)
315
- cxSmall, _, _ := procGetSystemMetrics.Call(smCXSmall)
316
- cySmall, _, _ := procGetSystemMetrics.Call(smCYSmall)
317
- big := loadExeIcon(cxIcon, cyIcon)
318
- small := loadExeIcon(cxSmall, cySmall)
319
- if big != 0 {
320
- procSendMessage.Call(hwnd, wmSetIcon, 1 /* ICON_BIG */, big)
321
- procDestroyIcon.Call(big)
322
- }
323
- if small != 0 {
324
- procSendMessage.Call(hwnd, wmSetIcon, 0 /* ICON_SMALL */, small)
325
- procDestroyIcon.Call(small)
326
- }
327
- }
328
-
329
- // dibToPNG 将 ICO 内嵌的 DIB 图像(BITMAPINFOHEADER 起始的裸 DIB,即 rcedit 注入
330
- // 非 PNG .ico 时的 RT_ICON 数据)纯 Go 解析为 PNG 字节,替代原 GDI
331
- // CreateDIBSection+DrawIconEx 绘制路径(消除对 gdi32/user32 绘图 API 的依赖)。
332
- // 支持 32/24 bpp;透明由 AND mask 决定(对应位=1 表示透明)。
333
- func dibToPNG(data []byte) ([]byte, error) {
334
- if len(data) < 40 {
335
- return nil, fmt.Errorf("DIB too short: %d bytes", len(data))
336
- }
337
- biSize := binary.LittleEndian.Uint32(data[0:4])
338
- if biSize < 40 {
339
- return nil, fmt.Errorf("unsupported BITMAPINFOHEADER size %d", biSize)
340
- }
341
- width := int(binary.LittleEndian.Uint32(data[4:8]))
342
- heightRaw := int32(binary.LittleEndian.Uint32(data[8:12]))
343
- bitCount := binary.LittleEndian.Uint16(data[14:16])
344
- if width <= 0 || heightRaw == 0 {
345
- return nil, fmt.Errorf("bad dimensions %dx%d", width, heightRaw)
346
- }
347
- bpp := (int(bitCount) + 7) / 8 // bytes per pixel
348
- if bpp != 3 && bpp != 4 {
349
- return nil, fmt.Errorf("unsupported bitcount %d", bitCount)
350
- }
351
- height := int(heightRaw)
352
- if height < 0 {
353
- height = -height
354
- }
355
- // ICO 的 DIB 高度可能为实际像素高度的 2 倍(上方 XOR + 下方 AND mask),
356
- // 此时需折半,否则图像会被纵向拉伸。
357
- pixelRowBytes := (width*bpp + 3) &^ 3 // 每行按 4 字节对齐
358
- andRowBytes := ((width + 31) / 32) * 4
359
- pxLen := pixelRowBytes * height
360
- if pxLen > len(data)-int(biSize) && height%2 == 0 {
361
- height /= 2
362
- pxLen = pixelRowBytes * height
363
- }
364
- pxStart := int(biSize)
365
- andStart := pxStart + pxLen
366
- // AND mask 是否存在以实际数据长度为准(H1):ICO 的 DIB 若数据不足
367
- // (无 AND mask 段),andStart+andRowBytes*height 会超出 len(data),
368
- // 此时下方读取 data[andRow+x/8] 将越界 panic。
369
- // 此前以 heightRaw>0 判定恒为真(上方已排除 heightRaw==0),
370
- // 无 mask 的 ICO 会命中越界分支导致窗口控制崩壳。
371
- hasAndMask := andStart+andRowBytes*height <= len(data)
372
- img := image.NewRGBA(image.Rect(0, 0, width, height))
373
- for y := 0; y < height; y++ {
374
- srcY := height - 1 - y // DIB 自底向上:数据首行是图像末行
375
- row := pxStart + srcY*pixelRowBytes
376
- andRow := andStart + srcY*andRowBytes
377
- for x := 0; x < width; x++ {
378
- off := row + x*bpp
379
- if off+bpp > len(data) {
380
- return nil, fmt.Errorf("DIB data truncated at x=%d,y=%d", x, y)
381
- }
382
- b := data[off]
383
- g := data[off+1]
384
- r := data[off+2]
385
- a := uint8(255)
386
- if hasAndMask && (data[andRow+x/8]>>uint(7-x%8))&1 == 1 {
387
- a = 0
388
- }
389
- img.SetRGBA(x, y, color.RGBA{r, g, b, a})
526
+ case TitleBarHidden:
527
+ // 隐藏标题栏视觉但保留系统原生按钮:DWM 玻璃扩展。
528
+ // 标题栏区域透明化并并入客户区,右上角的最小化 / 最大化 / 关闭按钮
529
+ // 由 DWM 继续原生绘制,标题文字置空。
530
+ // 标题栏区域并入客户区须扩展顶部(cyTopHeight),扩展底部不影响标题栏视觉。
531
+ m := margins{cxLeftWidth: 0, cxRightWidth: 0, cyTopHeight: 1, cyBottomHeight: 0}
532
+ procDwmExtendFrameIntoArea.Call(hwnd, uintptr(unsafe.Pointer(&m)))
533
+ // 标题文字一并清除,标题栏区域只保留系统按钮
534
+ if title, err := syscall.UTF16PtrFromString(""); err == nil {
535
+ procSetWindowText.Call(hwnd, uintptr(unsafe.Pointer(title)))
390
536
  }
537
+ refreshFrame(hwnd)
538
+ default: // TitleBarNative:不处理
391
539
  }
392
- var buf bytes.Buffer
393
- if err := png.Encode(&buf, img); err != nil {
394
- return nil, err
395
- }
396
- return buf.Bytes(), nil
397
540
  }