b6619ef53b
- app.go: App struct with stub bindings (RunCheck/StartEngine/
StopEngine/GetStatus/Version) — emits check:result, check:done,
engine:status, stats:update events. Real backend lands in Phase 1.
- run.go: wails.Run() with frameless 480x640 fixed window, Classic
dark bg matching theme.
- embed.go: //go:embed all:frontend/dist for the Vite build output.
- frontend/: Vite + React project derived from `wails init -t react`.
Removed default template assets and wired Classic variant from
docs/design/v2/.
- components/Classic.jsx: variant 1 with custom title bar
(drag region, sun/moon theme toggle, min/close hooked to
Wails WindowMinimise/Quit).
- components/shared.jsx: useDrover hook adapted to call Wails
bindings and listen on backend events instead of mock SCENARIOS.
Added IconSun + IconMoon for the title-bar toggle.
- App.jsx: owns mode state, wraps setMode in
document.startViewTransition so the title-bar toggle gives a
circle-reveal sweep from the cursor.
- style.css: clean reset (overflow hidden, no scrollbars, brand
background) — replaces the wails-react-template defaults.
- wailsjs/go/gui/App.js: hand-written bindings since our App
struct lives in package gui rather than the standard top-level
main; `wails generate module` would have written package main
bindings here.
- build/: standard wails artifacts (icon, manifest); will be
consumed by `wails build` once we wire it through CI.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
173 lines
5.2 KiB
Go
173 lines
5.2 KiB
Go
// 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)
|
|
}
|