Add standalone Go build alongside VSCode extension
Ports all five tools to a single 7.5 MB Windows exe with an embedded WebView2 window, file-based config, single-instance focus, and clean shutdown. Adds build.ps1 to build both targets from one command. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
171
standalone/main.go
Normal file
171
standalone/main.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
webview "github.com/jchv/go-webview2"
|
||||
)
|
||||
|
||||
var currentHwnd atomic.Uintptr
|
||||
|
||||
func setupFileLogging() {
|
||||
logPath := filepath.Join(appDir(), "debug.log")
|
||||
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.SetOutput(f)
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
||||
}
|
||||
|
||||
func main() {
|
||||
setupFileLogging()
|
||||
log.Printf("HPL Toolbox starting (pid=%d)", os.Getpid())
|
||||
|
||||
if focusExistingInstance() {
|
||||
log.Printf("Another instance is running; focused it and exiting.")
|
||||
return
|
||||
}
|
||||
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
log.Printf("Server listening on 127.0.0.1:%d", port)
|
||||
|
||||
mux := buildMux()
|
||||
srv := &http.Server{Handler: mux}
|
||||
go func() {
|
||||
if err := srv.Serve(listener); err != nil && err != http.ErrServerClosed {
|
||||
log.Printf("Server error: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := writeLockFile(port); err != nil {
|
||||
log.Printf("Failed to write lock file: %v", err)
|
||||
}
|
||||
defer removeLockFile()
|
||||
|
||||
w := webview.NewWithOptions(webview.WebViewOptions{
|
||||
Debug: false,
|
||||
AutoFocus: true,
|
||||
WindowOptions: webview.WindowOptions{
|
||||
Title: "HPL Toolbox",
|
||||
Width: 1100,
|
||||
Height: 720,
|
||||
IconId: 1,
|
||||
Center: true,
|
||||
},
|
||||
})
|
||||
if w == nil {
|
||||
log.Fatalln("Failed to create webview.")
|
||||
}
|
||||
defer w.Destroy()
|
||||
|
||||
// Stash HWND for the /api/focus handler.
|
||||
currentHwnd.Store(uintptr(w.Window()))
|
||||
|
||||
w.SetSize(1100, 720, webview.HintNone)
|
||||
w.Navigate(fmt.Sprintf("http://127.0.0.1:%d/", port))
|
||||
w.Run()
|
||||
|
||||
// Window closed — shut the server down cleanly.
|
||||
log.Printf("Window closed; shutting down server.")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
func buildMux() *http.ServeMux {
|
||||
mux := http.NewServeMux()
|
||||
|
||||
// Pages
|
||||
mux.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" && r.URL.Path != "/home" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
HomePage(w, r)
|
||||
})
|
||||
mux.HandleFunc("GET /plec", PlecPage)
|
||||
mux.HandleFunc("GET /applovin", ApplovinPage)
|
||||
mux.HandleFunc("GET /base64", Base64Page)
|
||||
mux.HandleFunc("GET /daily", DailyPage)
|
||||
mux.HandleFunc("GET /mobile", MobilePage)
|
||||
mux.HandleFunc("GET /assets/qrcode.min.js", MobileQrScript)
|
||||
|
||||
// Shared API
|
||||
mux.HandleFunc("POST /api/clipboard", ClipboardEndpoint)
|
||||
mux.HandleFunc("POST /api/open", OpenEndpoint)
|
||||
mux.HandleFunc("POST /api/focus", FocusEndpoint)
|
||||
|
||||
// Daily Update
|
||||
mux.HandleFunc("POST /api/daily/submit", DailySubmit)
|
||||
|
||||
// PLEC
|
||||
mux.HandleFunc("POST /api/plec/pick", PlecPick)
|
||||
mux.HandleFunc("POST /api/plec/upload", PlecUpload)
|
||||
|
||||
// AppLovin
|
||||
mux.HandleFunc("POST /api/applovin/pick", ApplovinPick)
|
||||
mux.HandleFunc("POST /api/applovin/upload", ApplovinUpload)
|
||||
|
||||
// Base64
|
||||
mux.HandleFunc("POST /api/base64/pickFolder", Base64PickFolder)
|
||||
mux.HandleFunc("POST /api/base64/pickFiles", Base64PickFiles)
|
||||
mux.HandleFunc("POST /api/base64/scan", Base64Scan)
|
||||
|
||||
// Send To Mobile
|
||||
mux.HandleFunc("POST /api/mobile/pick", MobilePick)
|
||||
mux.HandleFunc("POST /api/mobile/start", MobileStart)
|
||||
mux.HandleFunc("POST /api/mobile/stop", MobileStop)
|
||||
|
||||
return mux
|
||||
}
|
||||
|
||||
// FocusEndpoint brings the webview window to the foreground. Used by a second
|
||||
// instance that wants to focus the running one before exiting.
|
||||
func FocusEndpoint(w http.ResponseWriter, r *http.Request) {
|
||||
hwnd := currentHwnd.Load()
|
||||
if hwnd != 0 {
|
||||
restoreAndFocus(hwnd)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procShowWindow = user32.NewProc("ShowWindow")
|
||||
procSetForeground = user32.NewProc("SetForegroundWindow")
|
||||
procIsIconic = user32.NewProc("IsIconic")
|
||||
)
|
||||
|
||||
const (
|
||||
swRestore = 9
|
||||
swShow = 5
|
||||
)
|
||||
|
||||
func restoreAndFocus(hwnd uintptr) {
|
||||
// If minimized, restore. Otherwise just ensure it's shown.
|
||||
iconic, _, _ := procIsIconic.Call(hwnd)
|
||||
if iconic != 0 {
|
||||
_, _, _ = procShowWindow.Call(hwnd, swRestore)
|
||||
} else {
|
||||
_, _, _ = procShowWindow.Call(hwnd, swShow)
|
||||
}
|
||||
_, _, _ = procSetForeground.Call(hwnd)
|
||||
}
|
||||
|
||||
// Silence "imported and not used" complaints if any.
|
||||
var _ = unsafe.Sizeof(int(0))
|
||||
Reference in New Issue
Block a user