// Package gui hosts the Wails app: the App struct (whose exported methods // become the JS API for the frontend) and the Run() helper invoked from // cmd/drover/main.go when the user double-clicks the binary. package gui import ( "context" "fmt" "math/rand" "sync" "time" "github.com/wailsapp/wails/v2/pkg/runtime" ) // App is the Wails-bound struct. Every exported method is callable from JS // via the auto-generated wailsjs/go/main/App.* bindings. // // Right now everything except the proxy form is a deterministic stub — // the real WinDivert + SOCKS5 engine arrives in Phase 1. The stubs are // sufficient for the UI to feel alive: Check fakes a 7-step diagnostic, // Start/Stop toggles a phase, GetStats emits realistic-looking numbers. type App struct { ctx context.Context version string mu sync.Mutex running bool startedAt time.Time } // NewApp returns a fresh App stamped with the binary's build version // (so the GUI can display it in the title bar). func NewApp(version string) *App { return &App{version: version} } // Version returns the build version (e.g. "0.2.0", "test-local", or // "dev"). Frontend reads it on mount to populate the custom title bar. func (a *App) Version() string { return a.version } // Startup is called by Wails right after the window is created and the // JS runtime is ready. We grab the context for runtime.EventsEmit calls // from any subsequent method. func (a *App) Startup(ctx context.Context) { a.ctx = ctx go a.statsLoop() } // Config is the proxy/auth payload the frontend sends back from the form. type Config struct { Host string `json:"host"` Port int `json:"port"` Auth bool `json:"auth"` Login string `json:"login"` Password string `json:"password"` } // CheckResult is one row in the diagnostic table; the frontend listens // for them on the "check:result" event. type CheckResult struct { ID string `json:"id"` // tcp / greet / auth / connect / udp / stun / api Status string `json:"status"` // running | passed | failed | skipped Metric string `json:"metric"` Error string `json:"error,omitempty"` Hint string `json:"hint,omitempty"` Duration int64 `json:"duration_ms,omitempty"` } // RunCheck plays out a fake 7-step diagnostic. Each result is pushed to // the JS side as a "check:result" event; when the run finishes we emit // "check:done" with the overall summary. // // Phase 1+: replace this with internal/checker.Run() and stream its // real results. func (a *App) RunCheck(cfg Config) { go func() { ids := []string{"tcp", "greet"} if cfg.Auth { ids = append(ids, "auth") } ids = append(ids, "connect", "udp", "stun", "api") fakeMetric := map[string]string{ "tcp": "14 ms", "greet": "SOCKS5/0x05", "auth": "user/pass · ok", "connect": "gateway.discord.gg", "udp": "relay 1.2.3.4:54321", "stun": "38 ms RTT", "api": "204 OK · 89 ms", } passedCount := 0 for _, id := range ids { runtime.EventsEmit(a.ctx, "check:result", CheckResult{ID: id, Status: "running"}) time.Sleep(350 * time.Millisecond) // Stub: everything passes. Real checker will fail UDP if proxy // rejects ASSOCIATE, etc. runtime.EventsEmit(a.ctx, "check:result", CheckResult{ ID: id, Status: "passed", Metric: fakeMetric[id], Duration: 350, }) passedCount++ } runtime.EventsEmit(a.ctx, "check:done", map[string]int{ "total": len(ids), "passed": passedCount, "failed": len(ids) - passedCount, }) }() } // StartEngine flips the proxy on. In the stub we just toggle the flag and // note the start time so GetStats can produce a believable uptime. func (a *App) StartEngine() error { a.mu.Lock() defer a.mu.Unlock() a.running = true a.startedAt = time.Now() runtime.EventsEmit(a.ctx, "engine:status", map[string]any{"running": true}) return nil } // StopEngine turns the proxy off. func (a *App) StopEngine() error { a.mu.Lock() defer a.mu.Unlock() a.running = false runtime.EventsEmit(a.ctx, "engine:status", map[string]any{"running": false}) return nil } // GetStatus is read by the frontend on first paint to know whether to // show "Idle" or "Active". func (a *App) GetStatus() map[string]any { a.mu.Lock() defer a.mu.Unlock() return map[string]any{ "running": a.running, "uptimeS": int(time.Since(a.startedAt).Seconds()), } } // statsLoop emits a stats event every second when the engine is running. // Numbers are random but stable enough to look real. func (a *App) statsLoop() { r := rand.New(rand.NewSource(time.Now().UnixNano())) tick := time.NewTicker(time.Second) defer tick.Stop() for range tick.C { a.mu.Lock() if !a.running || a.ctx == nil { a.mu.Unlock() continue } uptime := int(time.Since(a.startedAt).Seconds()) a.mu.Unlock() runtime.EventsEmit(a.ctx, "stats:update", map[string]any{ "up": r.Intn(50_000) + 5_000, // bytes/sec out "down": r.Intn(500_000) + 50_000, // bytes/sec in "tcp": r.Intn(8) + 1, "udp": r.Intn(5) + 1, "uptimeS": uptime, }) } } // Greet remains as a smoke check that the bindings pipeline survived // the transition. Frontend can call it from a debug button if needed. func (a *App) Greet(name string) string { return fmt.Sprintf("Hello %s — Drover-Go GUI is alive.", name) }