diff --git a/cmd/routatic-proxy/main.go b/cmd/routatic-proxy/main.go index 61c1ff5..5fb4a41 100644 --- a/cmd/routatic-proxy/main.go +++ b/cmd/routatic-proxy/main.go @@ -349,6 +349,7 @@ Press Ctrl+C to stop the server.`, defer cancel() var guiSrv *gui.Server + var guiDone <-chan struct{} // Start proxy in background. go func() { @@ -389,9 +390,14 @@ Press Ctrl+C to stop the server.`, return fmt.Errorf("start gui server: %w", err) } - // Open GUI (macOS: native webview, Linux/Windows: print URL) - if err := openGUI(guiURL); err != nil { - slog.Warn("GUI error", "error", err) + // Open GUI (macOS: system tray + browser, Linux/Windows: print URL). + // guiDone is closed when the user quits from the tray (CGO + // path); it is nil on non-CGO platforms where only SIGINT + // stops the proxy. + var guiErr error + guiDone, guiErr = openGUI(guiURL) + if guiErr != nil { + slog.Warn("GUI error", "error", guiErr) fmt.Printf("\nDashboard: %s\n", guiURL) fmt.Println("\nPress Ctrl+C to stop.") } @@ -399,13 +405,16 @@ Press Ctrl+C to stop the server.`, fmt.Println("\nRunning in headless mode (no dashboard). Press Ctrl+C to stop.") } - // Wait for signal. + // Wait for signal or GUI window close. sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM) select { case <-sigCh: fmt.Println("\nShutting down...") case <-ctx.Done(): + case <-guiDone: + fmt.Println("\nGUI window closed, shutting down...") + cancel() } // Graceful shutdown. diff --git a/cmd/routatic-proxy/start_gui_darwin.go b/cmd/routatic-proxy/start_gui_darwin.go index 4124c52..7bafb8c 100644 --- a/cmd/routatic-proxy/start_gui_darwin.go +++ b/cmd/routatic-proxy/start_gui_darwin.go @@ -4,39 +4,39 @@ package main import ( "fmt" + "os/exec" - "github.com/getlantern/systray" - "github.com/webview/webview_go" + "github.com/routatic/proxy/internal/tray" ) -func openGUI(guiURL string) error { +// openGUI starts the system tray icon and opens the dashboard in the default +// browser. It returns a channel that is closed when the user clicks "Quit" in +// the tray menu, signaling that the proxy should shut down. +// +// systray.Run must be called from the main OS thread on macOS (AppKit +// requirement). The goroutine that calls this function must be the main +// goroutine locked to the OS thread via runtime.LockOSThread, OR the library +// handles it internally (getlantern/systray does). +func openGUI(guiURL string) (<-chan struct{}, error) { fmt.Printf("\nDashboard: %s\n", guiURL) - fmt.Println("Opening native window...") - - // Set up system tray first (initializes on main thread), then start webview - // run loop inside onReady so both coexist: tray menu + native webview window. - systray.Run(func() { - systray.SetTitle("routatic-proxy") - systray.SetTooltip("routatic-proxy is running") - mQuit := systray.AddMenuItem("Quit", "Stop the proxy") - - wv := webview.New(false) - wv.SetTitle("routatic-proxy") - wv.SetSize(1200, 800, webview.HintNone) - wv.Navigate(guiURL) - - go func() { - <-mQuit.ClickedCh - wv.Dispatch(func() { - wv.Terminate() - }) - systray.Quit() - }() - - wv.Run() - wv.Destroy() - systray.Quit() - }, nil) - - return nil + + // Open dashboard in default browser. + _ = exec.Command("open", guiURL).Start() + + done := make(chan struct{}) + + go func() { + tray.Run(tray.Callbacks{ + InitiallyRunning: true, + InitiallyAutostart: false, + OnOpen: func() { + _ = exec.Command("open", guiURL).Start() + }, + OnQuit: func() { + close(done) + }, + }) + }() + + return done, nil } diff --git a/cmd/routatic-proxy/start_gui_darwin_nocgo.go b/cmd/routatic-proxy/start_gui_darwin_nocgo.go index ca5fdac..82a5b52 100644 --- a/cmd/routatic-proxy/start_gui_darwin_nocgo.go +++ b/cmd/routatic-proxy/start_gui_darwin_nocgo.go @@ -2,12 +2,12 @@ package main -import ( - "fmt" -) +import "fmt" -func openGUI(guiURL string) error { +// openGUI prints the dashboard URL. It returns a nil channel because there is +// no native window to wait on — only SIGINT stops the proxy. +func openGUI(guiURL string) (<-chan struct{}, error) { fmt.Printf("Dashboard: %s\n", guiURL) fmt.Println("\nPress Ctrl+C to stop.") - return nil + return nil, nil } diff --git a/cmd/routatic-proxy/start_gui_other.go b/cmd/routatic-proxy/start_gui_other.go index 3c34da8..61ff496 100644 --- a/cmd/routatic-proxy/start_gui_other.go +++ b/cmd/routatic-proxy/start_gui_other.go @@ -2,12 +2,12 @@ package main -import ( - "fmt" -) +import "fmt" -func openGUI(guiURL string) error { +// openGUI prints the dashboard URL. It returns a nil channel because there is +// no native window to wait on — only SIGINT stops the proxy. +func openGUI(guiURL string) (<-chan struct{}, error) { fmt.Printf("Dashboard: %s\n", guiURL) fmt.Println("\nPress Ctrl+C to stop.") - return nil + return nil, nil } diff --git a/go.mod b/go.mod index 517fa2c..28f7b1a 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,6 @@ require ( github.com/getlantern/systray v1.2.2 github.com/pkoukk/tiktoken-go v0.1.8 github.com/spf13/cobra v1.8.1 - github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 golang.org/x/mod v0.38.0 golang.org/x/sys v0.46.0 modernc.org/sqlite v1.53.0 diff --git a/go.sum b/go.sum index cda55e6..15c215e 100644 --- a/go.sum +++ b/go.sum @@ -56,8 +56,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6 h1:VQpB2SpK88C6B5lPHTuSZKb2Qee1QWwiFlC5CKY4AW0= -github.com/webview/webview_go v0.0.0-20240831120633-6173450d4dd6/go.mod h1:yE65LFCeWf4kyWD5re+h4XNvOHJEXOCOuJZ4v8l5sgk= golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=