feat: Phase 1 WinAuth Go 移植完整实现

将原 C#/.NET WinAuth 移植为 Go + Gio GUI,覆盖 Phase 1 全部功能。

核心模块:
- internal/authenticator: TOTP (Google/Microsoft/Okta) + HOTP + BattleNet + Steam,含
  enroll/sync/code 生成、Steam 交易确认轮询
- internal/config: YAML 配置 + 老版 WinAuth XML 导入(DPAPI + Password + Blowfish/PBKDF2 解密链)
- internal/crypto: 现代加密 (WAGO1) + DPAPI 跨平台封装 + 老版 Blowfish ECB
- internal/win32: 单实例 Mutex 锁 + 全局热键管理器 (RegisterHotKey + PeekMessage 泵) +
  SendInput Unicode 注入 + 剪贴板文本/CF_DIB 图像读写 + AttachThreadInput 焦点切换
- internal/hotkey: "Ctrl+Alt+G" 风格快捷键字符串解析/格式化
- internal/qr: gozxing 二维码解码 + otpauth:// URI 解析
- internal/i18n: en/zh-CN/de 三语 TOML

UI 模块 (Gio):
- 主窗口:圆环倒计时进度条、复制按钮 + Toast 反馈、空列表占位、行分隔线
- 添加流程:vendor 菜单 + 各 vendor 独立对话框 + 二维码扫描入口(文件 / 剪贴板)
- 设置:密码加密、老版 XML 导入、每条目热键配置
- Steam:注册向导(含 captcha/email/SMS 多步)+ 交易确认窗

构建:Windows 主目标,非 Windows 平台所有 Win32 功能走 build-tag 桩实现。
This commit is contained in:
2026-06-12 03:10:37 +08:00
commit c671f2115e
165 changed files with 10102 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
//go:build !windows
package win32
import "image"
// GetClipboardImage is a no-op on non-Windows builds.
func GetClipboardImage() (image.Image, error) {
return nil, ErrUnsupported
}
+130
View File
@@ -0,0 +1,130 @@
//go:build windows
package win32
import (
"fmt"
"image"
"image/color"
"unsafe"
)
// GetClipboardImage attempts to read an image from the Windows clipboard.
// It supports CF_DIB and CF_DIBV5 (the two formats produced by Snipping
// Tool / Win+Shift+S and most other screenshot tools). Returns nil, nil
// if the clipboard does not currently hold an image format we can read.
func GetClipboardImage() (image.Image, error) {
if err := openClipboardRetry(0); err != nil {
return nil, fmt.Errorf("win32: OpenClipboard: %w", err)
}
defer procCloseClipboard.Call()
// Try CF_DIBV5 (17) first, then CF_DIB (8).
if img, err := readDIBFromClipboard(17); err != nil {
return nil, err
} else if img != nil {
return img, nil
}
if img, err := readDIBFromClipboard(8); err != nil {
return nil, err
} else if img != nil {
return img, nil
}
return nil, nil
}
func readDIBFromClipboard(format uint32) (image.Image, error) {
r, _, _ := procGetClipboardData.Call(uintptr(format))
if r == 0 {
return nil, nil
}
src, _, _ := procGlobalLock.Call(r)
if src == 0 {
return nil, nil
}
defer procGlobalUnlock.Call(r)
size, _, _ := procGlobalSize.Call(r)
if size == 0 {
return nil, nil
}
data := unsafe.Slice((*byte)(unsafe.Pointer(src)), size)
img := decodeDIB(data)
if img == nil {
return nil, fmt.Errorf("win32: unsupported DIB format")
}
return img, nil
}
// decodeDIB decodes a BITMAPINFOHEADER-based DIB blob (no BMP file
// header) into an *image.RGBA. Only uncompressed 24-bit and 32-bit
// pixel formats are supported, which covers what the Windows snipping
// stack produces.
func decodeDIB(dib []byte) image.Image {
if len(dib) < 40 {
return nil
}
headerSize := int(readU32(dib, 0))
width := int(int32(readU32(dib, 4)))
height := int(int32(readU32(dib, 8)))
bitCount := int(readU16(dib, 14))
compression := int(readU32(dib, 16))
if width <= 0 || height == 0 {
return nil
}
if compression != 0 || (bitCount != 24 && bitCount != 32) {
return nil
}
absH := height
topDown := false
if absH < 0 {
absH = -absH
topDown = true
}
rowBytes := ((bitCount*width + 31) / 32) * 4
pixelOffset := headerSize
// BITMAPV4/V5 headers may include color masks immediately after, but
// since we only accept BI_RGB, headerSize already points past them.
if len(dib) < pixelOffset+rowBytes*absH {
return nil
}
img := image.NewRGBA(image.Rect(0, 0, width, absH))
for y := 0; y < absH; y++ {
var srcY int
if topDown {
srcY = y
} else {
srcY = absH - 1 - y
}
row := dib[pixelOffset+srcY*rowBytes:]
for x := 0; x < width; x++ {
off := x * (bitCount / 8)
b := row[off]
g := row[off+1]
r := row[off+2]
var a byte = 0xff
if bitCount == 32 {
// CF_DIB normally writes 0 in the alpha slot; treat 0
// as opaque to avoid an invisible bitmap.
ax := row[off+3]
if ax != 0 {
a = ax
}
}
img.Set(x, y, color.RGBA{R: r, G: g, B: b, A: a})
}
}
return img
}
func readU16(b []byte, off int) uint16 {
return uint16(b[off]) | uint16(b[off+1])<<8
}
func readU32(b []byte, off int) uint32 {
return uint32(b[off]) | uint32(b[off+1])<<8 | uint32(b[off+2])<<16 | uint32(b[off+3])<<24
}
+15
View File
@@ -0,0 +1,15 @@
// Package win32 wraps the small set of Win32 APIs winauth-go needs for
// platform-specific UX: single-instance locking, global hotkeys, key
// injection, clipboard access, and foreground-window juggling.
//
// All exported functions are no-ops or return ErrUnsupported on
// non-Windows platforms. Callers should treat failure as informational
// (warn + continue) rather than fatal — none of this functionality is
// load-bearing for the core authenticator code.
package win32
import "errors"
// ErrUnsupported is returned by every entry point when the build is not
// Windows. Callers should test with errors.Is.
var ErrUnsupported = errors.New("win32: feature only available on Windows")
+26
View File
@@ -0,0 +1,26 @@
package win32
// Hotkey describes a single global key combination. Mods is the bitwise
// OR of ModCtrl/ModAlt/ModShift/ModWin; VK is a virtual-key code
// (0x41='A', ...).
type Hotkey struct {
Mods uint32
VK uint32
}
// Modifier bits passed to RegisterHotKey.
const (
ModAlt uint32 = 0x1
ModCtrl uint32 = 0x2
ModShift uint32 = 0x4
ModWin uint32 = 0x8
// ModNoRepeat suppresses auto-repeat events when the key is held.
// Supported on Windows 7+.
ModNoRepeat uint32 = 0x4000
)
// HotkeyEvent fires when a registered hotkey is pressed. ID matches the
// id returned by HotkeyManager.Register.
type HotkeyEvent struct {
ID int32
}
+19
View File
@@ -0,0 +1,19 @@
//go:build !windows
package win32
// HotkeyManager is the cross-platform stub. Register always errors;
// Events returns a never-firing channel.
type HotkeyManager struct {
events chan HotkeyEvent
}
func NewHotkeyManager() *HotkeyManager {
return &HotkeyManager{events: make(chan HotkeyEvent)}
}
func (m *HotkeyManager) Events() <-chan HotkeyEvent { return m.events }
func (m *HotkeyManager) Register(h Hotkey) (int32, error) { return 0, ErrUnsupported }
func (m *HotkeyManager) Unregister(id int32) error { return ErrUnsupported }
func (m *HotkeyManager) Stop() { close(m.events) }
+203
View File
@@ -0,0 +1,203 @@
//go:build windows
package win32
import (
"fmt"
"runtime"
"sync"
"syscall"
"unsafe"
"golang.org/x/sys/windows"
)
// HotkeyManager owns a dedicated OS thread that runs a GetMessageW
// pump. RegisterHotKey can only be called from the thread that will
// receive the messages, so all register/unregister/dispatch operations
// are serialized onto that goroutine via the cmd channel.
type HotkeyManager struct {
cmd chan hkCmd
events chan HotkeyEvent
mu sync.Mutex
nextID int32
stopped bool
}
type hkCmd struct {
kind hkCmdKind
id int32
hotkey Hotkey
reply chan error
}
type hkCmdKind int
const (
hkCmdRegister hkCmdKind = iota
hkCmdUnregister
hkCmdStop
)
// NewHotkeyManager starts the dedicated thread and returns a manager
// ready to accept Register calls. Events() yields presses; the channel
// is closed on Stop().
func NewHotkeyManager() *HotkeyManager {
m := &HotkeyManager{
cmd: make(chan hkCmd),
events: make(chan HotkeyEvent, 16),
nextID: 1,
}
started := make(chan struct{})
go m.run(started)
<-started
return m
}
// Events returns the read-only event channel.
func (m *HotkeyManager) Events() <-chan HotkeyEvent { return m.events }
// Register adds a global hotkey and returns its assigned id. Re-registering
// a combination that is already taken by another application returns an
// error; the caller should surface it to the user.
func (m *HotkeyManager) Register(h Hotkey) (int32, error) {
m.mu.Lock()
if m.stopped {
m.mu.Unlock()
return 0, fmt.Errorf("win32: hotkey manager stopped")
}
id := m.nextID
m.nextID++
m.mu.Unlock()
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdRegister, id: id, hotkey: h, reply: reply}
if err := <-reply; err != nil {
return 0, err
}
return id, nil
}
// Unregister removes a previously registered hotkey.
func (m *HotkeyManager) Unregister(id int32) error {
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdUnregister, id: id, reply: reply}
return <-reply
}
// Stop tears down the message pump and closes Events(). Subsequent
// Register calls fail.
func (m *HotkeyManager) Stop() {
m.mu.Lock()
if m.stopped {
m.mu.Unlock()
return
}
m.stopped = true
m.mu.Unlock()
reply := make(chan error, 1)
m.cmd <- hkCmd{kind: hkCmdStop, reply: reply}
<-reply
}
// run is the dedicated-thread loop. It owns the message queue that
// RegisterHotKey targets.
func (m *HotkeyManager) run(started chan struct{}) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Force the message queue to exist before anybody tries to post to
// us. PeekMessage with PM_NOREMOVE is the canonical incantation.
var msg msgStruct
procPeekMessageW.Call(
uintptr(unsafe.Pointer(&msg)),
0, 0, 0, 0, // PM_NOREMOVE
)
close(started)
for {
// Non-blocking message pump: drain hotkey messages first, then
// service one cmd, then sleep briefly. A blocking GetMessage
// would freeze the cmd intake.
for {
r, _, _ := procPeekMessageW.Call(
uintptr(unsafe.Pointer(&msg)),
0, 0, 0, 1, // PM_REMOVE
)
if r == 0 {
break
}
if msg.message == wmHotKey {
select {
case m.events <- HotkeyEvent{ID: int32(msg.wParam)}:
default:
// Listener slow — drop to avoid stalling the pump.
}
}
}
select {
case c := <-m.cmd:
switch c.kind {
case hkCmdRegister:
err := registerHotKey(0, c.id, c.hotkey.Mods, c.hotkey.VK)
c.reply <- err
case hkCmdUnregister:
err := unregisterHotKey(0, c.id)
c.reply <- err
case hkCmdStop:
close(m.events)
c.reply <- nil
return
}
default:
// brief sleep so we don't busy-loop. 30ms is well under any
// human-perceptible hotkey latency.
windows.SleepEx(30, false)
}
}
}
// -----------------------------------------------------------------------------
// raw syscalls
var (
procRegisterHotKey = user32.NewProc("RegisterHotKey")
procUnregisterHotKey = user32.NewProc("UnregisterHotKey")
procPeekMessageW = user32.NewProc("PeekMessageW")
)
const wmHotKey uint32 = 0x0312
type msgStruct struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt struct{ x, y int32 }
}
func registerHotKey(hwnd uintptr, id int32, mods, vk uint32) error {
r, _, e := procRegisterHotKey.Call(
hwnd, uintptr(id), uintptr(mods), uintptr(vk),
)
if r == 0 {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: RegisterHotKey: %w", errno)
}
return fmt.Errorf("win32: RegisterHotKey: unknown failure")
}
return nil
}
func unregisterHotKey(hwnd uintptr, id int32) error {
r, _, e := procUnregisterHotKey.Call(hwnd, uintptr(id))
if r == 0 {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: UnregisterHotKey: %w", errno)
}
}
return nil
}
+23
View File
@@ -0,0 +1,23 @@
//go:build !windows
package win32
// SetClipboardText errors on non-Windows; callers should warn and fall
// back to displaying the code in the UI.
func SetClipboardText(s string) error { return ErrUnsupported }
// GetForegroundWindow returns 0 on non-Windows so Auto-type sites can
// detect the absence and skip.
func GetForegroundWindow() uintptr { return 0 }
// FocusWindow is a no-op on non-Windows.
func FocusWindow(hwnd uintptr) error { return ErrUnsupported }
// TypeUnicode is a no-op on non-Windows.
func TypeUnicode(s string) error { return ErrUnsupported }
// PressKey is a no-op on non-Windows.
func PressKey(vk uint16) error { return ErrUnsupported }
// VK_RETURN is exposed for cross-platform compile.
const VK_RETURN uint16 = 0x0D
+214
View File
@@ -0,0 +1,214 @@
//go:build windows
package win32
import (
"fmt"
"syscall"
"time"
"unicode/utf16"
"unsafe"
"golang.org/x/sys/windows"
)
// SetClipboardText copies s onto the Windows clipboard as CF_UNICODETEXT.
// OpenClipboard may transiently fail if another process holds the
// clipboard; we retry a few times before giving up.
func SetClipboardText(s string) error {
const cfUnicodeText = 13
utf := utf16.Encode([]rune(s + "\x00"))
size := len(utf) * 2
hMem, _, e := procGlobalAlloc.Call(0x0042 /*GMEM_MOVEABLE|GMEM_ZEROINIT*/, uintptr(size))
if hMem == 0 {
return fmt.Errorf("win32: GlobalAlloc: %w", e)
}
dst, _, _ := procGlobalLock.Call(hMem)
if dst == 0 {
procGlobalFree.Call(hMem)
return fmt.Errorf("win32: GlobalLock failed")
}
dstSlice := unsafe.Slice((*uint16)(unsafe.Pointer(dst)), len(utf))
copy(dstSlice, utf)
procGlobalUnlock.Call(hMem)
if err := openClipboardRetry(0); err != nil {
procGlobalFree.Call(hMem)
return err
}
procEmptyClipboard.Call()
r, _, ce := procSetClipboardData.Call(cfUnicodeText, hMem)
if r == 0 {
procCloseClipboard.Call()
procGlobalFree.Call(hMem)
return fmt.Errorf("win32: SetClipboardData: %w", ce)
}
// Ownership of hMem transfers to the system on success — do not free.
procCloseClipboard.Call()
return nil
}
func openClipboardRetry(hwnd uintptr) error {
var last error
for i := 0; i < 8; i++ {
r, _, e := procOpenClipboard.Call(hwnd)
if r != 0 {
return nil
}
last = e
time.Sleep(20 * time.Millisecond)
}
return fmt.Errorf("win32: OpenClipboard: %w", last)
}
// GetForegroundWindow returns the HWND that currently has keyboard
// focus. Use this to remember the target window before the user clicks
// into winauth-go (which itself becomes foreground and would otherwise
// receive injected keystrokes).
func GetForegroundWindow() uintptr {
r, _, _ := procGetForegroundWindow.Call()
return r
}
// FocusWindow restores hwnd to the foreground. Hits the well-known
// SetForegroundWindow restriction (only the foreground process may
// hand focus to another); we work around it by attaching to the target
// thread's input queue briefly, the trick documented in MSDN's
// "AttachThreadInput" page.
func FocusWindow(hwnd uintptr) error {
if hwnd == 0 {
return fmt.Errorf("win32: FocusWindow: nil hwnd")
}
curTID, _, _ := procGetCurrentThreadId.Call()
targetTID, _, _ := procGetWindowThreadProcessId.Call(hwnd, 0)
if targetTID == 0 {
return fmt.Errorf("win32: GetWindowThreadProcessId failed")
}
if curTID != targetTID {
procAttachThreadInput.Call(curTID, targetTID, 1)
defer procAttachThreadInput.Call(curTID, targetTID, 0)
}
procSetForegroundWindowProc.Call(hwnd)
procShowWindowProc.Call(hwnd, 9 /*SW_RESTORE*/)
procBringWindowToTop.Call(hwnd)
return nil
}
// TypeUnicode injects s as Unicode characters using SendInput KEYEVENTF_UNICODE.
// Special characters in s pass through transparently; no translation
// of newlines / tabs happens. If you need an Enter at the end, pass
// "\n" and let the caller append it explicitly.
func TypeUnicode(s string) error {
if s == "" {
return nil
}
utf := utf16.Encode([]rune(s))
// Each rune becomes 2 inputs (keydown + keyup).
inputs := make([]inputUnion, 0, len(utf)*2)
for _, u := range utf {
inputs = append(inputs,
makeUnicodeInput(u, false),
makeUnicodeInput(u, true),
)
}
r, _, e := procSendInput.Call(
uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0]),
)
if int(r) != len(inputs) {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: SendInput: sent %d/%d: %w", r, len(inputs), errno)
}
return fmt.Errorf("win32: SendInput: sent %d/%d", r, len(inputs))
}
return nil
}
// PressKey injects a single virtual-key down+up pair (e.g. VK_RETURN).
func PressKey(vk uint16) error {
inputs := [2]inputUnion{
makeVKInput(vk, false),
makeVKInput(vk, true),
}
r, _, e := procSendInput.Call(
uintptr(len(inputs)),
uintptr(unsafe.Pointer(&inputs[0])),
unsafe.Sizeof(inputs[0]),
)
if int(r) != len(inputs) {
if errno, ok := e.(syscall.Errno); ok && errno != 0 {
return fmt.Errorf("win32: SendInput (vk): %w", errno)
}
return fmt.Errorf("win32: SendInput (vk): short send")
}
return nil
}
// VK_RETURN is exposed for callers that want to press Enter after Auto-type.
const VK_RETURN uint16 = 0x0D
// -----------------------------------------------------------------------------
// raw syscalls + structs
var (
procOpenClipboard = user32.NewProc("OpenClipboard")
procCloseClipboard = user32.NewProc("CloseClipboard")
procEmptyClipboard = user32.NewProc("EmptyClipboard")
procSetClipboardData = user32.NewProc("SetClipboardData")
procGetClipboardData = user32.NewProc("GetClipboardData")
procGetForegroundWindow = user32.NewProc("GetForegroundWindow")
procSetForegroundWindowProc = user32.NewProc("SetForegroundWindow")
procShowWindowProc = user32.NewProc("ShowWindow")
procBringWindowToTop = user32.NewProc("BringWindowToTop")
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
procAttachThreadInput = user32.NewProc("AttachThreadInput")
procSendInput = user32.NewProc("SendInput")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
procGlobalAlloc = kernel32.NewProc("GlobalAlloc")
procGlobalFree = kernel32.NewProc("GlobalFree")
procGlobalLock = kernel32.NewProc("GlobalLock")
procGlobalUnlock = kernel32.NewProc("GlobalUnlock")
procGlobalSize = kernel32.NewProc("GlobalSize")
procGetCurrentThreadId = kernel32.NewProc("GetCurrentThreadId")
)
// inputUnion is the Win32 INPUT structure, sized for KEYBDINPUT (the
// largest variant on x64 is MOUSEINPUT but KEYBDINPUT padded to 40
// works because INPUT_KEYBOARD never reads the extra trailing bytes).
type inputUnion struct {
typ uint32
_pad uint32 // alignment on 64-bit
wVk uint16
wScan uint16
dwFlags uint32
time uint32
dwExtra uintptr
// 8 bytes of MOUSEINPUT-sized padding so the layout is large enough
// for the union on amd64.
_padTail [8]byte
}
const (
inputKeyboard uint32 = 1
keyeventfKeyUp uint32 = 0x0002
keyeventfUnicode uint32 = 0x0004
)
func makeUnicodeInput(r uint16, up bool) inputUnion {
flags := keyeventfUnicode
if up {
flags |= keyeventfKeyUp
}
return inputUnion{typ: inputKeyboard, wScan: r, dwFlags: flags}
}
func makeVKInput(vk uint16, up bool) inputUnion {
var flags uint32
if up {
flags = keyeventfKeyUp
}
return inputUnion{typ: inputKeyboard, wVk: vk, dwFlags: flags}
}
+14
View File
@@ -0,0 +1,14 @@
//go:build !windows
package win32
// AcquireInstanceLock always reports "first instance" on non-Windows
// platforms — there is no convenient cross-platform equivalent and
// running two copies is not catastrophic for the YAML store (last
// writer wins).
func AcquireInstanceLock(name string) (release func(), alreadyRunning bool, err error) {
return func() {}, false, nil
}
// ActivateOtherInstance is a no-op on non-Windows platforms.
func ActivateOtherInstance(windowTitle string) error { return ErrUnsupported }
+96
View File
@@ -0,0 +1,96 @@
//go:build windows
package win32
import (
"fmt"
"syscall"
"time"
"unsafe"
"golang.org/x/sys/windows"
)
// AcquireInstanceLock takes a named mutex so a second launch of
// winauth-go can detect the first one. Returns a Release closer to be
// deferred at program exit. If another instance is already running it
// returns alreadyRunning=true and a zero Release; the caller may then
// call ActivateOtherInstance(windowTitle) to bring the existing window
// forward and exit.
func AcquireInstanceLock(name string) (release func(), alreadyRunning bool, err error) {
wname, err := syscall.UTF16PtrFromString(name)
if err != nil {
return nil, false, fmt.Errorf("win32: utf16: %w", err)
}
h, err := windows.CreateMutex(nil, false, wname)
if h == 0 {
return nil, false, fmt.Errorf("win32: CreateMutex: %w", err)
}
// CreateMutex returns the existing handle when the name is taken; in
// that case GetLastError reports ERROR_ALREADY_EXISTS. err is set
// regardless of whether the handle is valid, so check the error
// number rather than err != nil.
if errno, ok := err.(syscall.Errno); ok && errno == windows.ERROR_ALREADY_EXISTS {
windows.CloseHandle(h)
return nil, true, nil
}
return func() { windows.CloseHandle(h) }, false, nil
}
// ActivateOtherInstance walks the top-level windows looking for one
// whose title matches windowTitle, then restores + foregrounds it. The
// match is exact. Used after AcquireInstanceLock reports a duplicate
// launch.
func ActivateOtherInstance(windowTitle string) error {
wtitle, err := syscall.UTF16PtrFromString(windowTitle)
if err != nil {
return err
}
hwnd, _ := findWindow(nil, wtitle)
if hwnd == 0 {
// The mutex says somebody is running, but the window isn't up
// yet — give it a brief moment then retry once.
time.Sleep(200 * time.Millisecond)
hwnd, _ = findWindow(nil, wtitle)
}
if hwnd == 0 {
return fmt.Errorf("win32: could not locate existing window %q", windowTitle)
}
const (
SW_RESTORE = 9
)
showWindow(hwnd, SW_RESTORE)
setForegroundWindow(hwnd)
return nil
}
// -----------------------------------------------------------------------------
// raw syscall stubs — the windows package does not expose these directly.
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procFindWindowW = user32.NewProc("FindWindowW")
procShowWindow = user32.NewProc("ShowWindow")
procSetForegroundWindow = user32.NewProc("SetForegroundWindow")
)
func findWindow(class, title *uint16) (windows.HWND, error) {
r, _, e := procFindWindowW.Call(
uintptr(unsafe.Pointer(class)),
uintptr(unsafe.Pointer(title)),
)
if r == 0 {
return 0, e
}
return windows.HWND(r), nil
}
func showWindow(hwnd windows.HWND, cmdShow int32) bool {
r, _, _ := procShowWindow.Call(uintptr(hwnd), uintptr(cmdShow))
return r != 0
}
func setForegroundWindow(hwnd windows.HWND) bool {
r, _, _ := procSetForegroundWindow.Call(uintptr(hwnd))
return r != 0
}