Simplify GUI settings and build flags

This commit is contained in:
every_holiday
2026-06-26 09:16:46 +09:00
parent 4d77e67870
commit ae426244e0
22 changed files with 846 additions and 237 deletions

View File

@@ -8,9 +8,12 @@ import (
"path/filepath"
"vrc_osc_go/internal/app"
"vrc_osc_go/internal/buildinfo"
)
func main() {
log.Printf("vrc_osc version=%s build=%s", buildinfo.Version, buildinfo.BuildTime)
log.Printf("runtime main begin")
var configPath string
var mode string
var noGUI bool
@@ -20,23 +23,33 @@ func main() {
flag.Parse()
if mode == "" && !noGUI {
log.Printf("launching gui companion")
launchGUI()
}
log.Printf("calling app.Run config=%s mode=%s noGUI=%v", configPath, mode, noGUI)
if err := app.Run(configPath, mode); err != nil {
log.SetOutput(os.Stderr)
log.Fatalf("vrc_osc_go: %v", err)
}
log.Printf("app.Run returned cleanly")
}
func launchGUI() {
exe, err := os.Executable()
if err != nil {
log.Printf("launchGUI: os.Executable failed: %v", err)
return
}
gui := filepath.Join(filepath.Dir(exe), "vrc_osc_gui.exe")
log.Printf("launchGUI: exe=%s gui=%s", exe, gui)
if _, err := os.Stat(gui); err != nil {
log.Printf("launchGUI: gui missing: %v", err)
return
}
_ = exec.Command(gui).Start()
if err := exec.Command(gui).Start(); err != nil {
log.Printf("launchGUI: start failed: %v", err)
return
}
log.Printf("launchGUI: started gui companion")
}

View File

@@ -3,12 +3,22 @@
package main
import (
"os"
"log"
"path/filepath"
"vrc_osc_go/internal/app"
)
func main() {
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe")
log.Fatal(runNativeGUI())
if exe, err := os.Executable(); err == nil {
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe exe="+exe+" dir="+filepath.Dir(exe))
} else {
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe exe=unknown err="+err.Error())
}
if err := runNativeGUI(); err != nil {
_ = app.AppendRuntimeLog("GUI", "runNativeGUI failed: "+err.Error())
log.Fatal(err)
}
_ = app.AppendRuntimeLog("GUI", "runNativeGUI returned cleanly")
}

View File

@@ -5,7 +5,9 @@ package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"syscall"
"unsafe"
)
@@ -55,6 +57,14 @@ func openBrowser(url string) {
_ = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
}
func trayIconPath() string {
exe, err := os.Executable()
if err != nil {
return filepath.Join("doc", "vrcworldtour_new.ico")
}
return filepath.Join(filepath.Dir(exe), "..", "doc", "vrcworldtour_new.ico")
}
func runTray(uiURL string, open func()) error {
user32 := syscall.NewLazyDLL("user32.dll")
shell32 := syscall.NewLazyDLL("shell32.dll")
@@ -72,7 +82,7 @@ func runTray(uiURL string, open func()) error {
setForegroundWindow := user32.NewProc("SetForegroundWindow")
trackPopupMenu := user32.NewProc("TrackPopupMenu")
showWindow := user32.NewProc("ShowWindow")
loadIcon := user32.NewProc("LoadIconW")
loadImage := user32.NewProc("LoadImageW")
shellNotifyIcon := shell32.NewProc("Shell_NotifyIconW")
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
@@ -119,7 +129,13 @@ func runTray(uiURL string, open func()) error {
className, _ := syscall.UTF16PtrFromString("VRC_OSC_TRAY")
windowTitle, _ := syscall.UTF16PtrFromString("VRC OSC Tray")
icon, _, _ := loadIcon.Call(0, 32512) // IDI_APPLICATION
iconPath := trayIconPath()
iconPathPtr, _ := syscall.UTF16PtrFromString(iconPath)
const (
lrLoadFromFile = 0x00000010
imageIcon = 1
)
icon, _, _ := loadImage.Call(0, uintptr(unsafe.Pointer(iconPathPtr)), imageIcon, 0, 0, lrLoadFromFile)
var hwnd uintptr
var menu uintptr

View File

@@ -45,7 +45,8 @@ var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\
var vrcLogTimePattern = regexp.MustCompile(`^(\d{4}\.\d{2}\.\d{2} \d{2}:\d{2}:\d{2})`)
var vrcJoinPattern = regexp.MustCompile(`OnPlayerJoined\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
var vrcLeftPattern = regexp.MustCompile(`OnPlayerLeft\s+(.+?)\s+\(usr_[0-9a-fA-F-]+\)`)
var hwndTopMostFlag uintptr = ^uintptr(0) - 1
var hwndTopMostFlag uintptr = ^uintptr(0)
var hwndNotTopMostFlag uintptr = ^uintptr(0) - 1
var hwndNoMove uintptr = 0x0002
var hwndNoSize uintptr = 0x0001
var hwndNoActivate uintptr = 0x0010
@@ -74,13 +75,10 @@ type guiApp struct {
tabJoin uintptr
tabTranslate uintptr
tabSettings uintptr
btnActiveWindow uintptr
btnTopMost uintptr
btnFontDown uintptr
btnFontUp uintptr
activeTab string
activeWindow bool
lastActiveWindow bool
topMost bool
lastTopMost bool
fontSize int
@@ -125,15 +123,12 @@ func runNativeGUI() error {
translateMessage := user32.NewProc("TranslateMessage")
dispatchMessage := user32.NewProc("DispatchMessageW")
postQuitMessage := user32.NewProc("PostQuitMessage")
setForegroundWindow := user32.NewProc("SetForegroundWindow")
bringWindowToTop := user32.NewProc("BringWindowToTop")
setTimer := user32.NewProc("SetTimer")
loadCursor := user32.NewProc("LoadCursorW")
setWindowText := user32.NewProc("SetWindowTextW")
getWindowRect := user32.NewProc("GetWindowRect")
isWindowVisible := user32.NewProc("IsWindowVisible")
isIconic := user32.NewProc("IsIconic")
getForegroundWindow := user32.NewProc("GetForegroundWindow")
getSystemMetrics := user32.NewProc("GetSystemMetrics")
getModuleHandle := kernel32.NewProc("GetModuleHandleW")
loadIcon := user32.NewProc("LoadIconW")
@@ -168,7 +163,6 @@ func runNativeGUI() error {
var app guiApp
guiCfg := loadGUISettings()
app.activeWindow = guiCfg.ActiveWindow
app.topMost = guiCfg.TopMost
if guiCfg.FontSize > 0 {
app.fontSize = guiCfg.FontSize
@@ -187,7 +181,6 @@ func runNativeGUI() error {
joinTitle, _ := syscall.UTF16PtrFromString("Join Log")
translateTitle, _ := syscall.UTF16PtrFromString("Translate")
settingsTitle, _ := syscall.UTF16PtrFromString("Settings")
activeWindowTitle, _ := syscall.UTF16PtrFromString("Active window")
topMostTitle, _ := syscall.UTF16PtrFromString("Top most")
fontDownTitle, _ := syscall.UTF16PtrFromString("A-")
fontUpTitle, _ := syscall.UTF16PtrFromString("A+")
@@ -198,35 +191,28 @@ func runNativeGUI() error {
app.rightHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(rightTitle)), wsVisible|wsChild|wsClipChildren, 370, 60, 330, 260, hwnd, idLog, hInstance, 0)
settingsPaneTitle, _ := syscall.UTF16PtrFromString("")
app.settingsPaneHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(rightClass)), uintptr(unsafe.Pointer(settingsPaneTitle)), wsVisible|wsChild|wsClipChildren, 370, 60, 330, 260, hwnd, idLog+1, hInstance, 0)
app.btnActiveWindow, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(activeWindowTitle)), wsVisible|wsChild, 390, 90, 130, 30, hwnd, 3004, hInstance, 0)
app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), wsVisible|wsChild, 390, 130, 120, 30, hwnd, 3005, hInstance, 0)
app.btnFontDown, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontDownTitle)), wsVisible|wsChild, 390, 175, 50, 30, hwnd, 3006, hInstance, 0)
app.btnFontUp, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontUpTitle)), wsVisible|wsChild, 445, 175, 50, 30, hwnd, 3007, hInstance, 0)
app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), wsVisible|wsChild, 390, 90, 120, 30, hwnd, 3005, hInstance, 0)
app.btnFontDown, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontDownTitle)), wsVisible|wsChild, 390, 135, 50, 30, hwnd, 3006, hInstance, 0)
app.btnFontUp, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontUpTitle)), wsVisible|wsChild, 445, 135, 50, 30, hwnd, 3007, hInstance, 0)
app.activeTab = "join"
app.activeWindow = true
app.lastActiveWindow = false
setTimer.Call(hwnd, timerRefresh, 2000, 0)
applyFont(&app)
refreshGUI(setWindowText, &app)
applyActiveWindow(hwnd, &app, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic)
applyTopMost(hwnd, &app)
ensureWindowVisible("startup", hwnd, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics)
logWindowState("after-ensure", hwnd, getWindowRect, isWindowVisible, isIconic)
return 0
case wmCommandGUI:
needsFullRefresh := false
switch uint16(wParam & 0xffff) {
case 3001:
app.activeTab = "join"
needsFullRefresh = true
case 3002:
app.activeTab = "translate"
needsFullRefresh = true
case 3003:
app.activeTab = "settings"
case 3004:
if !allowRapidSettingsAction(&app) {
return 0
}
app.activeWindow = !app.activeWindow
saveGUISettings(&app)
applyActiveWindow(hwnd, &app, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic)
case 3005:
if !allowRapidSettingsAction(&app) {
return 0
@@ -263,8 +249,13 @@ func runNativeGUI() error {
saveGUISettings(&app)
}
}
refreshGUI(setWindowText, &app)
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
if needsFullRefresh {
refreshGUI(setWindowText, &app)
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
} else {
refreshCommandUI(setWindowText, &app)
refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
}
return 0
case wmTimerGUI:
refreshGUI(setWindowText, &app)
@@ -364,14 +355,6 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
t, _ := syscall.UTF16PtrFromString(formatGuestPane("Join Log", current))
setWindowText.Call(app.leftHwnd, uintptr(unsafe.Pointer(t)))
}
if app.btnActiveWindow != 0 {
label := "Active window: OFF"
if app.activeWindow {
label = "Active window: ON"
}
t, _ := syscall.UTF16PtrFromString(label)
setWindowText.Call(app.btnActiveWindow, uintptr(unsafe.Pointer(t)))
}
if app.btnTopMost != 0 {
label := "Top most: OFF"
if app.topMost {
@@ -388,7 +371,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
showWindowProc.Call(app.settingsPaneHwnd, swShowControl)
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 370, 60, 330, 260, swpVisibleFlags)
}
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnFontDown, app.btnFontUp} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swShowControl)
}
@@ -397,7 +380,6 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
}
if app.rightHwnd != 0 {
state := readRuntimeSnapshot()
state["active_window"] = app.activeWindow
state["top_most"] = app.topMost
state["font_size"] = app.fontSize
rightText := formatRightPane(app.activeTab, state, currentWorldLabel(), instanceCount)
@@ -408,13 +390,27 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp) {
if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
}
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnFontDown, app.btnFontUp} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl)
}
}
}
func refreshCommandUI(setWindowText *syscall.LazyProc, app *guiApp) {
if app == nil || setWindowText == nil {
return
}
if app.btnTopMost != 0 {
label := "Top most: OFF"
if app.topMost {
label = "Top most: ON"
}
t, _ := syscall.UTF16PtrFromString(label)
setWindowText.Call(app.btnTopMost, uintptr(unsafe.Pointer(t)))
}
}
func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc, app *guiApp) {
if showWindowProc == nil || setWindowPosProc == nil || app == nil {
return
@@ -424,7 +420,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
showWindowProc.Call(app.settingsPaneHwnd, swShowControl)
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 370, 60, 330, 260, swpVisibleFlags)
}
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnFontDown, app.btnFontUp} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swShowControl)
}
@@ -435,7 +431,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
}
for _, hwnd := range []uintptr{app.btnActiveWindow, app.btnTopMost, app.btnFontDown, app.btnFontUp} {
for _, hwnd := range []uintptr{app.btnTopMost, app.btnFontDown, app.btnFontUp} {
if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl)
}
@@ -443,24 +439,6 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
guiLog("refreshSettingsControls hidden")
}
func applyActiveWindow(hwnd uintptr, app *guiApp, showWindowProc, setForegroundWindow, bringWindowToTop, setWindowPosProc, getForegroundWindow, getSystemMetrics, getWindowRect, isWindowVisible, isIconic *syscall.LazyProc) {
if hwnd == 0 || app.activeWindow == app.lastActiveWindow {
return
}
if app.activeWindow {
showWindowProc.Call(hwnd, swRestore)
setWindowPosProc.Call(hwnd, 0, 0, 0, 0, 0, swpShowFlags)
bringWindowToTop.Call(hwnd)
setForegroundWindow.Call(hwnd)
foreground, _, _ := getForegroundWindow.Call()
guiLog(fmt.Sprintf("applyActiveWindow on foreground=%d target=%d", foreground, hwnd))
ensureWindowVisible("active-window", hwnd, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics)
} else {
guiLog("applyActiveWindow off")
}
app.lastActiveWindow = app.activeWindow
}
func ensureWindowVisible(stage string, hwnd uintptr, showWindowProc, setWindowPosProc, getWindowRect, isWindowVisible, isIconic, getSystemMetrics *syscall.LazyProc) {
if hwnd == 0 || getSystemMetrics == nil {
return
@@ -524,7 +502,7 @@ func applyTopMost(hwnd uintptr, app *guiApp) {
if app.topMost {
setWindowPosProc.Call(hwnd, hwndTopMostFlag, 0, 0, 0, 0, swpFlags)
} else {
setWindowPosProc.Call(hwnd, 0, 0, 0, 0, 0, swpFlags)
setWindowPosProc.Call(hwnd, hwndNotTopMostFlag, 0, 0, 0, 0, swpFlags)
}
app.lastTopMost = app.topMost
}
@@ -814,7 +792,6 @@ func formatGuestPane(title string, items []userState) string {
for _, item := range present {
b.WriteString("・ ")
b.WriteString(item.Name)
b.WriteString(" [present]")
if !item.LastJoin.IsZero() {
b.WriteString(" ")
b.WriteString(item.LastJoin.Format("15:04:05"))
@@ -835,7 +812,6 @@ func formatGuestPane(title string, items []userState) string {
for _, item := range left {
b.WriteString("・ ")
b.WriteString(item.Name)
b.WriteString(" [gray]")
if !item.LastLeave.IsZero() {
b.WriteString(" ")
b.WriteString(timeAgo(item.LastLeave))
@@ -873,7 +849,7 @@ func currentSelfName() string {
func loadGUISettings() config.GUIConfig {
cfg, err := config.Load("")
if err != nil || cfg == nil {
return config.GUIConfig{ActiveWindow: true, FontSize: 18}
return config.GUIConfig{TopMost: false, FontSize: 18}
}
if cfg.GUI.FontSize <= 0 {
cfg.GUI.FontSize = 18
@@ -886,9 +862,8 @@ func saveGUISettings(app *guiApp) {
return
}
guiCfg := config.GUIConfig{
ActiveWindow: app.activeWindow,
TopMost: app.topMost,
FontSize: app.fontSize,
TopMost: app.topMost,
FontSize: app.fontSize,
}
if err := config.SaveGUI("", guiCfg); err != nil {
guiLog("saveGUISettings failed: " + err.Error())
@@ -941,13 +916,7 @@ func formatRightPane(tab string, state map[string]any, worldLabel string, curren
b.WriteString("\r\nWorld: ")
b.WriteString(worldLabel)
case "settings":
b.WriteString("Active window: ")
if stateBool(state, "active_window") {
b.WriteString("ON")
} else {
b.WriteString("OFF")
}
b.WriteString("\r\nTop most: ")
b.WriteString("Top most: ")
if stateBool(state, "top_most") {
b.WriteString("ON")
} else {

View File

@@ -0,0 +1,6 @@
//go:build windows
package main
// This file exists so the launcher package has a Windows build target
// alongside the generated .syso resource file.

View File

@@ -6,15 +6,18 @@ import (
"os"
"os/exec"
"path/filepath"
"strings"
"syscall"
"unsafe"
"vrc_osc_go/internal/app"
"vrc_osc_go/internal/buildinfo"
"vrc_osc_go/internal/common"
"vrc_osc_go/internal/update"
)
func main() {
_ = appendLauncherLog("LAUNCHER", "launcher main begin")
var baseURL string
var ownerRepo string
var assetName string
@@ -32,22 +35,34 @@ func main() {
}
if _, err := mgr.CheckAndUpdate(); err != nil {
log.Printf("auto update skipped: %v", err)
_ = appendLauncherLog("LAUNCHER", "auto update skipped: "+err.Error())
} else {
_ = appendLauncherLog("LAUNCHER", "auto update ok")
}
base := runtimeBaseDir()
exe := filepath.Join(base, "vrc_osc.exe")
gui := filepath.Join(base, "vrc_osc_gui.exe")
_ = appendLauncherLog("LAUNCHER", "runtimeBaseDir="+base)
if _, err := os.Stat(exe); err != nil {
_ = appendLauncherLog("LAUNCHER", "missing runtime exe: "+err.Error())
showError("vrc_osc_launcher", "missing exe: "+err.Error())
log.Fatalf("missing exe: %v", err)
}
_ = appendLauncherLog("LAUNCHER", "version="+buildinfo.Version+" build="+buildinfo.BuildTime+" base="+base+" exe="+exe+" gui="+gui)
if _, err := os.Stat(gui); err == nil {
_ = appendLauncherLog("LAUNCHER", "starting gui companion")
_ = exec.Command(gui).Start()
} else {
_ = appendLauncherLog("LAUNCHER", "gui companion missing: "+err.Error())
}
args := append([]string{"--no-gui"}, os.Args[1:]...)
_ = appendLauncherLog("LAUNCHER", "launching runtime args="+strings.Join(args, " "))
if err := update.Launch(exe, args); err != nil {
_ = appendLauncherLog("LAUNCHER", "launch failed: "+err.Error())
showError("vrc_osc_launcher", "launch failed: "+err.Error())
log.Fatalf("launch failed: %v", err)
}
_ = appendLauncherLog("LAUNCHER", "runtime launch returned")
}
func showError(title, message string) {
@@ -73,3 +88,7 @@ func runtimeBaseDir() string {
}
return root
}
func appendLauncherLog(title, text string) error {
return app.AppendRuntimeLog(title, text)
}