@yufengtadian/freedom-cli 1.12.16 → 1.12.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -3
- package/lib/build.js +132 -12
- package/lib/cli.js +94 -4
- package/lib/config.js +12 -1
- package/lib/init.js +9 -1
- package/lib/security.js +150 -0
- package/lib/utils.js +5 -2
- package/lib/verify.js +236 -0
- package/package.json +39 -39
- package/shell/win-x64/freedom-shell.exe +0 -0
- package/templates/go/build_all.txt +0 -0
- package/templates/go/build_err.txt +0 -0
- package/templates/go/pkg/freedom/anti_debug_other.go +8 -0
- package/templates/go/pkg/freedom/anti_debug_windows.go +39 -0
- package/templates/go/pkg/freedom/assets/freedom.js +19 -16
- package/templates/go/pkg/freedom/backend_proc.go +7 -1
- package/templates/go/pkg/freedom/center_windows.go +36 -18
- package/templates/go/pkg/freedom/center_windows_test.go +32 -0
- package/templates/go/pkg/freedom/configfile.go +56 -7
- package/templates/go/pkg/freedom/freedom.go +70 -8
- package/templates/go/pkg/freedom/security.go +227 -0
- package/templates/go/pkg/freedom/security_test.go +153 -0
- package/templates/go/pkg/freedom/syscap_other.go +10 -0
- package/templates/go/pkg/freedom/syscap_windows.go +653 -0
- package/templates/go/pkg/freedom/tray_other.go +10 -0
- package/templates/go/pkg/freedom/tray_windows.go +506 -0
- package/templates/go/pkg/freedom/window_windows.go +28 -16
- package/templates/go/temp_vet_test/main.go +40 -0
- package/templates/go/test_err.txt +0 -0
- package/templates/go/vet_err.txt +1 -0
- package/templates/go/vet_err2.txt +2 -0
- package/templates/project/freedom.config.js +8 -0
- package/templates/project-minimal/freedom.config.js +58 -0
- package/templates/project-minimal/index.html +41 -0
- package/templates/project-minimal/package.json +14 -0
- package/templates/project-minimal/src/main.js +19 -0
- package/templates/project-minimal/vite.config.js +18 -0
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
//go:build windows
|
|
2
|
+
|
|
3
|
+
package freedom
|
|
4
|
+
|
|
5
|
+
// 系统托盘(Shell_NotifyIcon)与原生菜单栏(HMENU)。
|
|
6
|
+
//
|
|
7
|
+
// 托盘通过一个隐藏消息窗口接收回调消息(WM_APP+1),菜单项点击经 WM_COMMAND
|
|
8
|
+
// 分发,统一通过 App.Emit 推送到前端(freedom.tray.on('click'/'menu', ...))。
|
|
9
|
+
// 原生菜单栏通过 SetMenu 挂到主窗口,同样经 WM_COMMAND 回调。
|
|
10
|
+
//
|
|
11
|
+
// 与主窗口同一 UI 线程创建,消息由同一消息泵派发,无需额外线程。
|
|
12
|
+
|
|
13
|
+
import (
|
|
14
|
+
"encoding/json"
|
|
15
|
+
"fmt"
|
|
16
|
+
"sync"
|
|
17
|
+
"syscall"
|
|
18
|
+
"unsafe"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
// ---- 常量 ----
|
|
22
|
+
|
|
23
|
+
const (
|
|
24
|
+
wmApp = 0x8000 // WM_APP
|
|
25
|
+
wmCommand = 0x0111
|
|
26
|
+
wmLButtonUp = 0x0202
|
|
27
|
+
wmLButtonDbl = 0x0203
|
|
28
|
+
wmRButtonUp = 0x0205
|
|
29
|
+
wmNull = 0x0000
|
|
30
|
+
|
|
31
|
+
nimAdd = 0x00000000
|
|
32
|
+
nimModify = 0x00000001
|
|
33
|
+
nimDelete = 0x00000002
|
|
34
|
+
|
|
35
|
+
nifMessage = 0x00000001
|
|
36
|
+
nifIcon = 0x00000002
|
|
37
|
+
nifTip = 0x00000004
|
|
38
|
+
|
|
39
|
+
tpmRightButton = 0x00000002
|
|
40
|
+
tpmLeftAlign = 0x00000000
|
|
41
|
+
|
|
42
|
+
mfString = 0x00000000
|
|
43
|
+
mfSeparator = 0x00000800
|
|
44
|
+
mfChecked = 0x00000008
|
|
45
|
+
mfGrayed = 0x00000001
|
|
46
|
+
mfEnabled = 0x00000000
|
|
47
|
+
mfPopup = 0x00000010
|
|
48
|
+
mfByPosition = 0x00000400
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
var (
|
|
52
|
+
shell32 = syscall.NewLazyDLL("shell32.dll")
|
|
53
|
+
procShellNotifyIconW = shell32.NewProc("Shell_NotifyIconW")
|
|
54
|
+
|
|
55
|
+
procRegisterClassExW = user32win.NewProc("RegisterClassExW")
|
|
56
|
+
procCreateWindowExW = user32win.NewProc("CreateWindowExW")
|
|
57
|
+
procDefWindowProcW = user32win.NewProc("DefWindowProcW")
|
|
58
|
+
procCreatePopupMenu = user32win.NewProc("CreatePopupMenu")
|
|
59
|
+
procCreateMenu = user32win.NewProc("CreateMenu")
|
|
60
|
+
procAppendMenuW = user32win.NewProc("AppendMenuW")
|
|
61
|
+
procInsertMenuW = user32win.NewProc("InsertMenuW")
|
|
62
|
+
procDestroyMenu = user32win.NewProc("DestroyMenu")
|
|
63
|
+
procTrackPopupMenu = user32win.NewProc("TrackPopupMenu")
|
|
64
|
+
procGetCursorPos = user32win.NewProc("GetCursorPos")
|
|
65
|
+
procSetForegroundWindow = user32win.NewProc("SetForegroundWindow")
|
|
66
|
+
procSetMenu = user32win.NewProc("SetMenu")
|
|
67
|
+
procDrawMenuBar = user32win.NewProc("DrawMenuBar")
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
// ---- 结构 ----
|
|
71
|
+
|
|
72
|
+
type point struct {
|
|
73
|
+
X, Y int32
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
type wndClassExW struct {
|
|
77
|
+
CbSize uint32
|
|
78
|
+
Style uint32
|
|
79
|
+
LpfnWndProc uintptr
|
|
80
|
+
CbClsExtra int32
|
|
81
|
+
CbWndExtra int32
|
|
82
|
+
HInstance uintptr
|
|
83
|
+
HIcon uintptr
|
|
84
|
+
HCursor uintptr
|
|
85
|
+
HbrBackground uintptr
|
|
86
|
+
LpszMenuName *uint16
|
|
87
|
+
LpszClassName *uint16
|
|
88
|
+
HIconSm uintptr
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// notifyIconDataW 对应 NOTIFYICONDATAW(amd64 布局,cbSize=984)。
|
|
92
|
+
type notifyIconDataW struct {
|
|
93
|
+
CbSize uint32
|
|
94
|
+
_ uint32 // 对齐填充
|
|
95
|
+
HWnd uintptr
|
|
96
|
+
UID uint32
|
|
97
|
+
UFlags uint32
|
|
98
|
+
UCallbackMessage uint32
|
|
99
|
+
HIcon uintptr
|
|
100
|
+
SzTip [128]uint16
|
|
101
|
+
DwState uint32
|
|
102
|
+
DwStateMask uint32
|
|
103
|
+
SzInfo [256]uint16
|
|
104
|
+
UVersion uint32
|
|
105
|
+
SzInfoTitle [64]uint16
|
|
106
|
+
DwInfoFlags uint32
|
|
107
|
+
GuidItem [16]byte
|
|
108
|
+
HBalloonIcon uintptr
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// menuItem 描述一个托盘/菜单栏条目(JSON 透传)。
|
|
112
|
+
type menuItem struct {
|
|
113
|
+
ID string `json:"id"`
|
|
114
|
+
Label string `json:"label"`
|
|
115
|
+
Type string `json:"type"` // "item"(默认)/ "separator" / "submenu"
|
|
116
|
+
Enabled *bool `json:"enabled"` // nil=默认启用
|
|
117
|
+
Checked *bool `json:"checked"`
|
|
118
|
+
Submenu []menuItem `json:"submenu"`
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// trayState 保存当前托盘实例(单窗口框架内为单例)。
|
|
122
|
+
type trayState struct {
|
|
123
|
+
mu sync.Mutex
|
|
124
|
+
hwnd uintptr // 隐藏消息窗口
|
|
125
|
+
menu uintptr // 右键 popup 菜单
|
|
126
|
+
menuBar uintptr // 原生菜单栏 HMENU
|
|
127
|
+
items map[uint32]string // menu id -> 前端 id
|
|
128
|
+
nextID uint32
|
|
129
|
+
emit func(event string, data interface{})
|
|
130
|
+
icon uintptr // 当前托盘图标 HICON
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
var (
|
|
134
|
+
trayProcMu sync.Mutex
|
|
135
|
+
trayStateInst *trayState
|
|
136
|
+
trayWndProcCB uintptr
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
// trayProcCallback 是隐藏窗口的 WndProc(Go 回调,经 syscall.NewCallback 注册)。
|
|
140
|
+
// 由 RegisterClassEx 注册到系统,消息泵在 UI 线程派发。
|
|
141
|
+
func trayProcCallback(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
|
142
|
+
ts := trayStateInst
|
|
143
|
+
if ts == nil {
|
|
144
|
+
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
145
|
+
return r
|
|
146
|
+
}
|
|
147
|
+
switch msg {
|
|
148
|
+
case uint32(wmApp) + 1: // uCallbackMessage
|
|
149
|
+
switch lParam & 0xFFFF {
|
|
150
|
+
case wmLButtonUp:
|
|
151
|
+
ts.emit("tray:click", map[string]interface{}{"button": "left"})
|
|
152
|
+
case wmLButtonDbl:
|
|
153
|
+
ts.emit("tray:double-click", map[string]interface{}{"button": "left"})
|
|
154
|
+
case wmRButtonUp:
|
|
155
|
+
trayShowMenu(ts)
|
|
156
|
+
}
|
|
157
|
+
case uint32(wmCommand):
|
|
158
|
+
id := uint32(wParam & 0xFFFF)
|
|
159
|
+
ts.mu.Lock()
|
|
160
|
+
label, ok := ts.items[id]
|
|
161
|
+
ts.mu.Unlock()
|
|
162
|
+
if ok {
|
|
163
|
+
ts.emit("tray:menu", map[string]interface{}{"id": label})
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
r, _, _ := procDefWindowProcW.Call(hwnd, uintptr(msg), wParam, lParam)
|
|
167
|
+
return r
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// ensureTrayWindow 创建隐藏消息窗口(仅一次)。
|
|
171
|
+
func ensureTrayWindow(a *App) (*trayState, error) {
|
|
172
|
+
trayProcMu.Lock()
|
|
173
|
+
defer trayProcMu.Unlock()
|
|
174
|
+
if trayStateInst != nil {
|
|
175
|
+
return trayStateInst, nil
|
|
176
|
+
}
|
|
177
|
+
className, _ := syscall.UTF16PtrFromString("FreedomTrayWnd")
|
|
178
|
+
if trayWndProcCB == 0 {
|
|
179
|
+
trayWndProcCB = syscall.NewCallback(trayProcCallback)
|
|
180
|
+
}
|
|
181
|
+
hInst := uintptr(0)
|
|
182
|
+
if m, _, _ := procGetModuleHandle.Call(0); m != 0 {
|
|
183
|
+
hInst = m
|
|
184
|
+
}
|
|
185
|
+
wc := wndClassExW{
|
|
186
|
+
CbSize: uint32(unsafe.Sizeof(wndClassExW{})),
|
|
187
|
+
LpfnWndProc: trayWndProcCB,
|
|
188
|
+
HInstance: hInst,
|
|
189
|
+
LpszClassName: className,
|
|
190
|
+
}
|
|
191
|
+
if r, _, _ := procRegisterClassExW.Call(uintptr(unsafe.Pointer(&wc))); r == 0 {
|
|
192
|
+
// 可能已注册,继续尝试。
|
|
193
|
+
}
|
|
194
|
+
ts := &trayState{
|
|
195
|
+
items: map[uint32]string{},
|
|
196
|
+
nextID: 1,
|
|
197
|
+
emit: a.Emit,
|
|
198
|
+
}
|
|
199
|
+
hwnd, _, _ := procCreateWindowExW.Call(
|
|
200
|
+
0,
|
|
201
|
+
uintptr(unsafe.Pointer(className)),
|
|
202
|
+
uintptr(unsafe.Pointer(className)),
|
|
203
|
+
0,
|
|
204
|
+
0, 0, 0, 0,
|
|
205
|
+
0, 0, hInst, 0,
|
|
206
|
+
)
|
|
207
|
+
if hwnd == 0 {
|
|
208
|
+
return nil, fmt.Errorf("tray: 创建隐藏窗口失败")
|
|
209
|
+
}
|
|
210
|
+
ts.hwnd = hwnd
|
|
211
|
+
trayStateInst = ts
|
|
212
|
+
return ts, nil
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// traySetIcon 创建或更新托盘图标。
|
|
216
|
+
func traySetIcon(ts *trayState, iconData []byte, tooltip string) error {
|
|
217
|
+
ts.mu.Lock()
|
|
218
|
+
defer ts.mu.Unlock()
|
|
219
|
+
// 释放旧图标。
|
|
220
|
+
if ts.icon != 0 {
|
|
221
|
+
procDestroyIcon.Call(ts.icon)
|
|
222
|
+
ts.icon = 0
|
|
223
|
+
}
|
|
224
|
+
nid := ¬ifyIconDataW{
|
|
225
|
+
CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})),
|
|
226
|
+
HWnd: ts.hwnd,
|
|
227
|
+
UID: 1,
|
|
228
|
+
UFlags: nifMessage | nifIcon | nifTip,
|
|
229
|
+
UCallbackMessage: wmApp + 1,
|
|
230
|
+
}
|
|
231
|
+
if len(iconData) > 0 {
|
|
232
|
+
ic := hiconFromPNG(iconData)
|
|
233
|
+
if ic == 0 {
|
|
234
|
+
return fmt.Errorf("tray: 无法从图标数据创建 HICON")
|
|
235
|
+
}
|
|
236
|
+
nid.HIcon = ic
|
|
237
|
+
ts.icon = ic
|
|
238
|
+
} else {
|
|
239
|
+
// 无图标数据:使用 exe 内嵌图标。
|
|
240
|
+
cx, _, _ := procGetSystemMetrics.Call(smCXSmall)
|
|
241
|
+
cy, _, _ := procGetSystemMetrics.Call(smCYSmall)
|
|
242
|
+
if ic := loadExeIcon(cx, cy); ic != 0 {
|
|
243
|
+
nid.HIcon = ic
|
|
244
|
+
ts.icon = ic
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if tooltip != "" {
|
|
248
|
+
tp, _ := syscall.UTF16FromString(tooltip)
|
|
249
|
+
copy(nid.SzTip[:], tp[:min(len(tp), 127)])
|
|
250
|
+
}
|
|
251
|
+
// 首次 NIM_ADD,之后 NIM_MODIFY。
|
|
252
|
+
if ts.hwnd == 0 || !trayIconAdded {
|
|
253
|
+
trayIconAdded = true
|
|
254
|
+
r, _, _ := procShellNotifyIconW.Call(nimAdd, uintptr(unsafe.Pointer(nid)))
|
|
255
|
+
if r == 0 {
|
|
256
|
+
return fmt.Errorf("tray: Shell_NotifyIcon(NIM_ADD) 失败")
|
|
257
|
+
}
|
|
258
|
+
} else {
|
|
259
|
+
r, _, _ := procShellNotifyIconW.Call(nimModify, uintptr(unsafe.Pointer(nid)))
|
|
260
|
+
if r == 0 {
|
|
261
|
+
return fmt.Errorf("tray: Shell_NotifyIcon(NIM_MODIFY) 失败")
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return nil
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
var trayIconAdded bool
|
|
268
|
+
|
|
269
|
+
// trayShowMenu 在鼠标位置弹出右键菜单。
|
|
270
|
+
func trayShowMenu(ts *trayState) {
|
|
271
|
+
if ts == nil || ts.menu == 0 {
|
|
272
|
+
return
|
|
273
|
+
}
|
|
274
|
+
var pt point
|
|
275
|
+
procGetCursorPos.Call(uintptr(unsafe.Pointer(&pt)))
|
|
276
|
+
procSetForegroundWindow.Call(ts.hwnd)
|
|
277
|
+
procTrackPopupMenu.Call(
|
|
278
|
+
ts.menu, tpmRightButton|tpmLeftAlign,
|
|
279
|
+
uintptr(pt.X), uintptr(pt.Y), 0, ts.hwnd, 0,
|
|
280
|
+
)
|
|
281
|
+
procPostMessage.Call(ts.hwnd, wmNull, 0, 0)
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// buildMenu 递归构建 HMENU(用于托盘右键与菜单栏子菜单)。
|
|
285
|
+
// 返回菜单句柄与 id 映射(追加到 ts.items)。
|
|
286
|
+
func buildMenu(ts *trayState, items []menuItem) uintptr {
|
|
287
|
+
hMenu, _, _ := procCreatePopupMenu.Call()
|
|
288
|
+
if hMenu == 0 {
|
|
289
|
+
return 0
|
|
290
|
+
}
|
|
291
|
+
appendMenuItems(ts, hMenu, items, false)
|
|
292
|
+
return hMenu
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// appendMenuItems 把条目追加到指定菜单。
|
|
296
|
+
func appendMenuItems(ts *trayState, hMenu uintptr, items []menuItem, isMenuBar bool) {
|
|
297
|
+
for i, it := range items {
|
|
298
|
+
if it.Type == "separator" {
|
|
299
|
+
procAppendMenuW.Call(hMenu, mfSeparator, 0, 0)
|
|
300
|
+
continue
|
|
301
|
+
}
|
|
302
|
+
if it.Type == "submenu" || (isMenuBar && len(it.Submenu) > 0) {
|
|
303
|
+
sub := buildMenu(ts, it.Submenu)
|
|
304
|
+
if sub == 0 {
|
|
305
|
+
continue
|
|
306
|
+
}
|
|
307
|
+
label, _ := syscall.UTF16PtrFromString(it.Label)
|
|
308
|
+
procAppendMenuW.Call(hMenu, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
309
|
+
continue
|
|
310
|
+
}
|
|
311
|
+
var flags uintptr = mfString | mfEnabled
|
|
312
|
+
if it.Checked != nil && *it.Checked {
|
|
313
|
+
flags |= mfChecked
|
|
314
|
+
}
|
|
315
|
+
if it.Enabled != nil && !*it.Enabled {
|
|
316
|
+
flags |= mfGrayed
|
|
317
|
+
}
|
|
318
|
+
id := ts.nextID
|
|
319
|
+
ts.nextID++
|
|
320
|
+
ts.mu.Lock()
|
|
321
|
+
ts.items[id] = it.ID
|
|
322
|
+
ts.mu.Unlock()
|
|
323
|
+
label, _ := syscall.UTF16PtrFromString(it.Label)
|
|
324
|
+
if isMenuBar && i == 0 {
|
|
325
|
+
// 菜单栏首个条目用 MF_BYPOSITION 占位(顶层菜单由 SetMenu 接管)。
|
|
326
|
+
}
|
|
327
|
+
procAppendMenuW.Call(hMenu, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// traySetMenu 设置托盘右键菜单(整体替换)。
|
|
332
|
+
func traySetMenu(ts *trayState, items []menuItem) {
|
|
333
|
+
ts.mu.Lock()
|
|
334
|
+
old := ts.menu
|
|
335
|
+
ts.menu = 0
|
|
336
|
+
ts.mu.Unlock()
|
|
337
|
+
if old != 0 {
|
|
338
|
+
procDestroyMenu.Call(old)
|
|
339
|
+
}
|
|
340
|
+
if len(items) == 0 {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
h := buildMenu(ts, items)
|
|
344
|
+
ts.mu.Lock()
|
|
345
|
+
ts.menu = h
|
|
346
|
+
ts.mu.Unlock()
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// menuBarSet 设置主窗口原生菜单栏(整体替换)。
|
|
350
|
+
func menuBarSet(hwnd uintptr, ts *trayState, items []menuItem) {
|
|
351
|
+
ts.mu.Lock()
|
|
352
|
+
old := ts.menuBar
|
|
353
|
+
ts.menuBar = 0
|
|
354
|
+
ts.mu.Unlock()
|
|
355
|
+
if old != 0 {
|
|
356
|
+
procDestroyMenu.Call(old)
|
|
357
|
+
}
|
|
358
|
+
if len(items) == 0 {
|
|
359
|
+
procSetMenu.Call(hwnd, 0)
|
|
360
|
+
procDrawMenuBar.Call(hwnd)
|
|
361
|
+
return
|
|
362
|
+
}
|
|
363
|
+
hBar, _, _ := procCreateMenu.Call()
|
|
364
|
+
if hBar == 0 {
|
|
365
|
+
return
|
|
366
|
+
}
|
|
367
|
+
for _, top := range items {
|
|
368
|
+
label, _ := syscall.UTF16PtrFromString(top.Label)
|
|
369
|
+
if top.Type == "separator" {
|
|
370
|
+
continue
|
|
371
|
+
}
|
|
372
|
+
if len(top.Submenu) > 0 {
|
|
373
|
+
sub := buildMenu(ts, top.Submenu)
|
|
374
|
+
procAppendMenuW.Call(hBar, mfPopup|mfEnabled, sub, uintptr(unsafe.Pointer(label)))
|
|
375
|
+
} else {
|
|
376
|
+
// 顶层无子菜单:视为可点击菜单项。
|
|
377
|
+
var flags uintptr = mfString | mfEnabled
|
|
378
|
+
if top.Checked != nil && *top.Checked {
|
|
379
|
+
flags |= mfChecked
|
|
380
|
+
}
|
|
381
|
+
id := ts.nextID
|
|
382
|
+
ts.nextID++
|
|
383
|
+
ts.mu.Lock()
|
|
384
|
+
ts.items[id] = top.ID
|
|
385
|
+
ts.mu.Unlock()
|
|
386
|
+
procAppendMenuW.Call(hBar, flags, uintptr(id), uintptr(unsafe.Pointer(label)))
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
ts.mu.Lock()
|
|
390
|
+
ts.menuBar = hBar
|
|
391
|
+
ts.mu.Unlock()
|
|
392
|
+
procSetMenu.Call(hwnd, hBar)
|
|
393
|
+
procDrawMenuBar.Call(hwnd)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// trayDestroy 移除托盘图标并清理隐藏窗口。
|
|
397
|
+
func trayDestroy(ts *trayState) {
|
|
398
|
+
if ts == nil {
|
|
399
|
+
return
|
|
400
|
+
}
|
|
401
|
+
ts.mu.Lock()
|
|
402
|
+
defer ts.mu.Unlock()
|
|
403
|
+
if trayIconAdded {
|
|
404
|
+
nid := ¬ifyIconDataW{CbSize: uint32(unsafe.Sizeof(notifyIconDataW{})), HWnd: ts.hwnd, UID: 1}
|
|
405
|
+
procShellNotifyIconW.Call(nimDelete, uintptr(unsafe.Pointer(nid)))
|
|
406
|
+
trayIconAdded = false
|
|
407
|
+
}
|
|
408
|
+
if ts.menu != 0 {
|
|
409
|
+
procDestroyMenu.Call(ts.menu)
|
|
410
|
+
ts.menu = 0
|
|
411
|
+
}
|
|
412
|
+
if ts.menuBar != 0 {
|
|
413
|
+
procDestroyMenu.Call(ts.menuBar)
|
|
414
|
+
ts.menuBar = 0
|
|
415
|
+
}
|
|
416
|
+
if ts.icon != 0 {
|
|
417
|
+
procDestroyIcon.Call(ts.icon)
|
|
418
|
+
ts.icon = 0
|
|
419
|
+
}
|
|
420
|
+
if ts.hwnd != 0 {
|
|
421
|
+
procDestroyWindow.Call(ts.hwnd)
|
|
422
|
+
ts.hwnd = 0
|
|
423
|
+
}
|
|
424
|
+
trayStateInst = nil
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
var procDestroyWindow = user32win.NewProc("DestroyWindow")
|
|
428
|
+
|
|
429
|
+
// trayCall 处理前端 __freedom_tray 请求。
|
|
430
|
+
func (a *App) trayCall(method string, paramsJSON string) (result interface{}, err error) {
|
|
431
|
+
defer func() {
|
|
432
|
+
if r := recover(); r != nil {
|
|
433
|
+
result = nil
|
|
434
|
+
err = fmt.Errorf("freedom: tray method %q panicked: %v", method, r)
|
|
435
|
+
}
|
|
436
|
+
}()
|
|
437
|
+
|
|
438
|
+
var args map[string]json.RawMessage
|
|
439
|
+
if len(paramsJSON) > 0 && paramsJSON != "null" {
|
|
440
|
+
if err := json.Unmarshal([]byte(paramsJSON), &args); err != nil {
|
|
441
|
+
return nil, fmt.Errorf("freedom: tray method %q: invalid args: %w", method, err)
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
argStr := func(k string) string {
|
|
445
|
+
if v, ok := args[k]; ok {
|
|
446
|
+
var s string
|
|
447
|
+
if json.Unmarshal(v, &s) == nil {
|
|
448
|
+
return s
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
return ""
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
switch method {
|
|
455
|
+
case "tray.create":
|
|
456
|
+
ts, err := ensureTrayWindow(a)
|
|
457
|
+
if err != nil {
|
|
458
|
+
return nil, err
|
|
459
|
+
}
|
|
460
|
+
return nil, traySetIcon(ts, dataURLToBytes(argStr("icon")), argStr("tooltip"))
|
|
461
|
+
case "tray.destroy":
|
|
462
|
+
trayDestroy(trayStateInst)
|
|
463
|
+
return nil, nil
|
|
464
|
+
case "tray.tooltip":
|
|
465
|
+
ts := trayStateInst
|
|
466
|
+
if ts == nil {
|
|
467
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
468
|
+
}
|
|
469
|
+
return nil, traySetIcon(ts, nil, argStr("tooltip"))
|
|
470
|
+
case "tray.menu":
|
|
471
|
+
ts := trayStateInst
|
|
472
|
+
if ts == nil {
|
|
473
|
+
return nil, fmt.Errorf("tray: 托盘未创建")
|
|
474
|
+
}
|
|
475
|
+
var items []menuItem
|
|
476
|
+
if v, ok := args["items"]; ok {
|
|
477
|
+
if err := json.Unmarshal(v, &items); err != nil {
|
|
478
|
+
return nil, fmt.Errorf("tray: 菜单格式错误: %w", err)
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
traySetMenu(ts, items)
|
|
482
|
+
return nil, nil
|
|
483
|
+
case "menu.set":
|
|
484
|
+
ts := trayStateInst
|
|
485
|
+
if ts == nil {
|
|
486
|
+
ts, err = ensureTrayWindow(a)
|
|
487
|
+
if err != nil {
|
|
488
|
+
return nil, err
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
var items []menuItem
|
|
492
|
+
if v, ok := args["items"]; ok {
|
|
493
|
+
if err := json.Unmarshal(v, &items); err != nil {
|
|
494
|
+
return nil, fmt.Errorf("menu: 菜单格式错误: %w", err)
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
hwnd := a.WindowHandle()
|
|
498
|
+
if hwnd == 0 {
|
|
499
|
+
return nil, fmt.Errorf("menu: 窗口未就绪")
|
|
500
|
+
}
|
|
501
|
+
menuBarSet(hwnd, ts, items)
|
|
502
|
+
return nil, nil
|
|
503
|
+
default:
|
|
504
|
+
return nil, fmt.Errorf("freedom: unknown tray method %q", method)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
@@ -183,16 +183,20 @@ func exeIconData() []byte {
|
|
|
183
183
|
if err := windows.GetModuleHandleEx(0, nil, &hInst); err != nil {
|
|
184
184
|
return nil
|
|
185
185
|
}
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
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
|
|
194
|
+
}
|
|
195
|
+
if data, err := windows.LoadResourceData(hInst, resInfo); err == nil {
|
|
196
|
+
return data
|
|
197
|
+
}
|
|
194
198
|
}
|
|
195
|
-
return
|
|
199
|
+
return nil
|
|
196
200
|
}
|
|
197
201
|
|
|
198
202
|
// isPNG 判断字节流是否为 PNG 编码(rcedit 注入的 .ico 内嵌 PNG 时 RT_ICON 数据即 PNG)。
|
|
@@ -209,9 +213,15 @@ func loadExeIcon(cx, cy uintptr) uintptr {
|
|
|
209
213
|
if hInst == 0 {
|
|
210
214
|
return 0
|
|
211
215
|
}
|
|
212
|
-
//
|
|
213
|
-
|
|
214
|
-
|
|
216
|
+
// 先按 rcedit 注入约定 RT_GROUP_ICON ID=0 加载;失败回退 ID=1(M3:
|
|
217
|
+
// 与 appIcon 的 RT_ICON ID 假设解耦,兼容不同注入工具的资源 ID)。
|
|
218
|
+
for _, id := range []uintptr{0, 1} {
|
|
219
|
+
// LoadImageW(hInst, MAKEINTRESOURCE(id), IMAGE_ICON, cx, cy, LR_DEFAULTCOLOR)
|
|
220
|
+
if r, _, _ := procLoadImage.Call(hInst, id, 1 /*IMAGE_ICON*/, cx, cy, 0 /*LR_DEFAULTCOLOR*/); r != 0 {
|
|
221
|
+
return r
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return 0
|
|
215
225
|
}
|
|
216
226
|
|
|
217
227
|
// exeAppIconDataURL 从当前 exe 的 PE 资源图标(rcedit 注入的 RT_ICON)提取为 PNG data URL。
|
|
@@ -353,10 +363,12 @@ func dibToPNG(data []byte) ([]byte, error) {
|
|
|
353
363
|
}
|
|
354
364
|
pxStart := int(biSize)
|
|
355
365
|
andStart := pxStart + pxLen
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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)
|
|
360
372
|
img := image.NewRGBA(image.Rect(0, 0, width, height))
|
|
361
373
|
for y := 0; y < height; y++ {
|
|
362
374
|
srcY := height - 1 - y // DIB 自底向上:数据首行是图像末行
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package main
|
|
2
|
+
|
|
3
|
+
import "unsafe"
|
|
4
|
+
|
|
5
|
+
// 写法B:widl 风格链式解引用
|
|
6
|
+
func comVtableB(ppv uintptr, index uint32) uintptr {
|
|
7
|
+
vt := *(*uintptr)(*(*unsafe.Pointer)(unsafe.Pointer(ppv)))
|
|
8
|
+
return *(*uintptr)(unsafe.Pointer(vt + uintptr(index)*8))
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// 写法D:先转 unsafe.Pointer 立即传给子函数
|
|
12
|
+
func comVtableD(ppv uintptr, index uint32) uintptr {
|
|
13
|
+
return comVtableDPtr(unsafe.Pointer(ppv), index)
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
func comVtableDPtr(pp unsafe.Pointer, index uint32) uintptr {
|
|
17
|
+
vt := *(*uintptr)(pp)
|
|
18
|
+
return *(*uintptr)(unsafe.Add(unsafe.Pointer(vt), uintptr(index)*8))
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// 写法E:uintptr 参数直接转结构体指针再读字段
|
|
22
|
+
type unkBox struct{ v unsafe.Pointer }
|
|
23
|
+
|
|
24
|
+
func comVtableE(ppv uintptr, index uint32) uintptr {
|
|
25
|
+
b := (*unkBox)(unsafe.Pointer(ppv))
|
|
26
|
+
slots := unsafe.Slice((*uintptr)(b.v), int(index)+1)
|
|
27
|
+
return slots[index]
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 写法F:类型化数组头 + Slice
|
|
31
|
+
func comVtableF(ppv uintptr, index uint32) uintptr {
|
|
32
|
+
obj := (*[1]unsafe.Pointer)(unsafe.Pointer(ppv))
|
|
33
|
+
vt := *obj
|
|
34
|
+
slots := unsafe.Slice((*uintptr)(vt[0]), int(index)+1)
|
|
35
|
+
return slots[index]
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func main() {
|
|
39
|
+
_, _, _, _ = comVtableB(0, 0), comVtableD(0, 0), comVtableE(0, 0), comVtableF(0, 0)
|
|
40
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pkg\freedom\syscap_windows.go:56:30: possible misuse of unsafe.Pointer
|
|
@@ -18,6 +18,14 @@ export default {
|
|
|
18
18
|
// 是否开启 WebView 开发者工具。
|
|
19
19
|
debug: false,
|
|
20
20
|
|
|
21
|
+
// 安全模式,在终端随时可改,三选一:
|
|
22
|
+
// 'none' - 明文资源(默认,兼容历史产物)
|
|
23
|
+
// 'basic' - 明文资源 + 构建期加固提示
|
|
24
|
+
// 'high' - resources 整体加密为 app.bin(磁盘无明文 HTML/配置),
|
|
25
|
+
// 壳内存解密 + 完整性校验,另含 anti-debug / 进程隐藏
|
|
26
|
+
// 终端命令:freedom security <none|basic|high>,或在 freedom build 时传 --security <mode>
|
|
27
|
+
security: 'none',
|
|
28
|
+
|
|
21
29
|
// 标题栏策略,在终端随时可改,二选一:
|
|
22
30
|
// 'native' - 保留系统原生标题栏,标题栏图标与 exe 图标一致
|
|
23
31
|
// 'frameless' - 完全无边框,标题栏与 Windows 原生最小化 / 最大化 / 关闭按钮均不存在,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Freedom 应用配置(极简模板)。
|
|
2
|
+
// 构建时由 freedom CLI 读取,生成对应的壳层配置。
|
|
3
|
+
export default {
|
|
4
|
+
// 应用名(窗口标题 / 可执行文件名)。
|
|
5
|
+
name: 'freedom-app',
|
|
6
|
+
|
|
7
|
+
// 窗口初始尺寸(像素)。
|
|
8
|
+
width: 1024,
|
|
9
|
+
height: 720,
|
|
10
|
+
|
|
11
|
+
// 窗口最小尺寸(设为 0 表示不限制)。
|
|
12
|
+
minWidth: 400,
|
|
13
|
+
minHeight: 300,
|
|
14
|
+
|
|
15
|
+
// 启动时是否在屏幕居中。
|
|
16
|
+
center: true,
|
|
17
|
+
|
|
18
|
+
// 是否开启 WebView 开发者工具。
|
|
19
|
+
debug: false,
|
|
20
|
+
|
|
21
|
+
// 安全模式,在终端随时可改,三选一:
|
|
22
|
+
// 'none' - 明文资源(默认,兼容历史产物)
|
|
23
|
+
// 'basic' - 明文资源 + 构建期加固提示
|
|
24
|
+
// 'high' - resources 整体加密为 app.bin(磁盘无明文 HTML/配置),
|
|
25
|
+
// 壳内存解密 + 完整性校验,另含 anti-debug / 进程隐藏
|
|
26
|
+
// 终端命令:freedom security <none|basic|high>,或在 freedom build 时传 --security <mode>
|
|
27
|
+
security: 'none',
|
|
28
|
+
|
|
29
|
+
// 标题栏策略,在终端随时可改,二选一:
|
|
30
|
+
// 'native' - 保留系统原生标题栏(极简模板默认:不引入自绘标题栏,开箱即用)
|
|
31
|
+
// 'frameless' - 完全无边框,关闭 / 最大化 / 最小化按钮由前端自绘。
|
|
32
|
+
// 切换 frameless 后需在前端自行实现窗口控制按钮(完整示例见完整模板 full:
|
|
33
|
+
// freedom init --template full 生成的 index.html / src/main.js)。
|
|
34
|
+
// 终端命令:freedom titlebar <native|frameless>
|
|
35
|
+
titlebar: 'native',
|
|
36
|
+
|
|
37
|
+
// 应用图标(可执行文件 / .app 的图标):
|
|
38
|
+
// Windows 需 .ico(推荐含 16/32/48/256 多尺寸),构建时注入 exe 资源;
|
|
39
|
+
// macOS 需 .icns,构建时放入 .app/Contents/Resources 并写入 Info.plist。
|
|
40
|
+
// 值为相对项目根目录的路径或绝对路径,未配置则使用壳默认图标。
|
|
41
|
+
// 终端命令:freedom icon <path>(Windows 主用)
|
|
42
|
+
icon: undefined,
|
|
43
|
+
|
|
44
|
+
// 产物输出目录(相对项目根目录或绝对路径):
|
|
45
|
+
// 'dist' - 输出到 dist/ 子目录(默认)
|
|
46
|
+
// '.' - 直接输出到项目根目录(dist 的上级),exe 与单文件 index.html 就地生成
|
|
47
|
+
// 'build/app' 等任意路径亦可
|
|
48
|
+
// 注意:输出到项目根目录时不会覆盖项目源文件(index.html 仅保留 src 源文件)。
|
|
49
|
+
outDir: 'dist',
|
|
50
|
+
|
|
51
|
+
// 后端策略:
|
|
52
|
+
// undefined - 仅使用框架内置能力(ping / 窗口控制),不启动独立后端进程
|
|
53
|
+
// { command, args } - 启动任意语言后端进程(Go/Node/Python/Rust 均可),
|
|
54
|
+
// 经 stdin/stdout NDJSON 与壳通信,方法直接暴露给前端
|
|
55
|
+
// 示例(Node 后端):
|
|
56
|
+
// backend: { command: 'node', args: ['backend/main.mjs'] }
|
|
57
|
+
// backend: undefined,
|
|
58
|
+
};
|