Files
VRCWT-OSC/cmd/vrc_osc_gui/tray_windows.go
2026-06-25 02:36:55 +09:00

217 lines
5.6 KiB
Go

//go:build windows
package main
import (
"fmt"
"log"
"os/exec"
"syscall"
"unsafe"
)
const (
wmUser = 0x0400
wmTray = wmUser + 1
wmCommand = 0x0111
wmDestroy = 0x0002
wmClose = 0x0010
wmRButtonUp = 0x0205
wmLButtonUp = 0x0202
wmCreate = 0x0001
wmQuit = 0x0012
niAdd = 0x00000000
niModify = 0x00000001
niDelete = 0x00000002
nifMessage = 0x00000001
nifIcon = 0x00000002
nifTip = 0x00000004
nifInfo = 0x00000010
nimPopup = 0x00000000
miString = 0x00000000
miSeparator = 0x00000800
miDefault = 0x00001000
swHide = 0
idOpen = 1001
idExit = 1002
maxTip = 128
maxClassName = 64
)
type trayApp struct {
uiURL string
open func()
}
func newTray(uiURL string, open func()) *trayApp {
return &trayApp{uiURL: uiURL, open: open}
}
func (t *trayApp) Run() error {
return runTray(t.uiURL, t.open)
}
func openBrowser(url string) {
_ = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
}
func runTray(uiURL string, open func()) error {
user32 := syscall.NewLazyDLL("user32.dll")
shell32 := syscall.NewLazyDLL("shell32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll")
registerClass := user32.NewProc("RegisterClassW")
createWindowEx := user32.NewProc("CreateWindowExW")
defWindowProc := user32.NewProc("DefWindowProcW")
getMessage := user32.NewProc("GetMessageW")
translateMessage := user32.NewProc("TranslateMessage")
dispatchMessage := user32.NewProc("DispatchMessageW")
postQuitMessage := user32.NewProc("PostQuitMessage")
createPopupMenu := user32.NewProc("CreatePopupMenu")
appendMenu := user32.NewProc("AppendMenuW")
setForegroundWindow := user32.NewProc("SetForegroundWindow")
trackPopupMenu := user32.NewProc("TrackPopupMenu")
showWindow := user32.NewProc("ShowWindow")
loadIcon := user32.NewProc("LoadIconW")
shellNotifyIcon := shell32.NewProc("Shell_NotifyIconW")
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
type wndClassEx struct {
cbSize uint32
style uint32
lpfnWndProc uintptr
cbClsExtra int32
cbWndExtra int32
hInstance uintptr
hIcon uintptr
hCursor uintptr
hbrBackground uintptr
lpszMenuName *uint16
lpszClassName *uint16
hIconSm uintptr
}
type point struct{ X, Y int32 }
type msg struct {
hwnd uintptr
message uint32
wParam uintptr
lParam uintptr
time uint32
pt point
}
type notifyIconData struct {
cbSize uint32
hWnd uintptr
uID uint32
uFlags uint32
uCallbackMessage uint32
hIcon uintptr
szTip [maxTip]uint16
dwState uint32
dwStateMask uint32
szInfo [256]uint16
uTimeoutVersion uint32
szInfoTitle [64]uint16
dwInfoFlags uint32
guidItem [16]byte
hBalloonIcon uintptr
}
className, _ := syscall.UTF16PtrFromString("VRC_OSC_TRAY")
windowTitle, _ := syscall.UTF16PtrFromString("VRC OSC Tray")
icon, _, _ := loadIcon.Call(0, 32512) // IDI_APPLICATION
var hwnd uintptr
var menu uintptr
var nid notifyIconData
wndProc := syscall.NewCallback(func(hwnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
switch msg {
case wmCreate:
return 0
case wmTray:
switch lParam {
case wmLButtonUp:
if open != nil {
open()
}
case wmRButtonUp:
if menu == 0 {
m, _, _ := createPopupMenu.Call()
menu = m
openText, _ := syscall.UTF16PtrFromString("Open UI")
exitText, _ := syscall.UTF16PtrFromString("Exit")
appendMenu.Call(menu, miString|miDefault, idOpen, uintptr(unsafe.Pointer(openText)))
appendMenu.Call(menu, miString, idExit, uintptr(unsafe.Pointer(exitText)))
}
setForegroundWindow.Call(hwnd)
var p point
user32.NewProc("GetCursorPos").Call(uintptr(unsafe.Pointer(&p)))
trackPopupMenu.Call(menu, nimPopup, uintptr(p.X), uintptr(p.Y), 0, hwnd, 0)
}
return 0
case wmCommand:
switch uint16(wParam & 0xffff) {
case idOpen:
if open != nil {
open()
}
case idExit:
user32.NewProc("DestroyWindow").Call(hwnd)
}
return 0
case wmDestroy:
nid.cbSize = uint32(unsafe.Sizeof(nid))
nid.hWnd = hwnd
nid.uID = 1
shellNotifyIcon.Call(niDelete, uintptr(unsafe.Pointer(&nid)))
postQuitMessage.Call(0)
return 0
}
ret, _, _ := defWindowProc.Call(hwnd, uintptr(msg), wParam, lParam)
return ret
})
hInstance, _, _ := getModuleHandle.Call(0)
_, _, regErr := registerClass.Call(uintptr(unsafe.Pointer(&wndClassEx{
cbSize: uint32(unsafe.Sizeof(wndClassEx{})),
lpfnWndProc: wndProc,
hInstance: hInstance,
hIcon: icon,
lpszClassName: className,
})))
if regErr != nil {
return fmt.Errorf("RegisterClassW failed: %v", regErr)
}
hwnd, _, err := createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(windowTitle)), 0, 0, 0, 0, 0, 0, 0, hInstance, 0)
if hwnd == 0 {
return fmt.Errorf("CreateWindowExW failed: %v", err)
}
showWindow.Call(hwnd, swHide)
nid.cbSize = uint32(unsafe.Sizeof(nid))
nid.hWnd = hwnd
nid.uID = 1
nid.uFlags = nifMessage | nifIcon | nifTip
nid.uCallbackMessage = wmTray
nid.hIcon = icon
tip := syscall.StringToUTF16("VRC OSC")
copy(nid.szTip[:], tip)
if ok, _, _ := shellNotifyIcon.Call(niAdd, uintptr(unsafe.Pointer(&nid))); ok == 0 {
return fmt.Errorf("Shell_NotifyIconW add failed")
}
log.Printf("tray started")
var m msg
for {
r, _, _ := getMessage.Call(uintptr(unsafe.Pointer(&m)), 0, 0, 0)
if int32(r) <= 0 {
break
}
translateMessage.Call(uintptr(unsafe.Pointer(&m)))
dispatchMessage.Call(uintptr(unsafe.Pointer(&m)))
}
return nil
}