From 29cf3f1a9763c94675135df14b130df526526377 Mon Sep 17 00:00:00 2001 From: Nitish Reddy M Date: Mon, 24 Aug 2026 00:28:40 -0400 Subject: [PATCH] fix(desktop): open default browser on startup (#2095) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #934 ## Summary The Go desktop binary started its local server and printed the URL, but never opened a browser automatically. This PR: - opens the default browser after the listener successfully binds - uses the actual runtime URL - uses a stdlib-only cross-platform launcher - keeps browser-open failures non-fatal - adds `--no-browser` for headless/Docker use - leaves the normal Vite/web development flow unchanged The listener is created before launching the browser so the browser cannot race the server startup path. ## Platform behavior - Windows: `rundll32 url.dll,FileProtocolHandler` - macOS: `open` - Linux: `xdg-open` No shell command strings are used; arguments are passed directly through `exec.Command`. ## Testing - `go test ./...` - `go vet ./...` - `go build ./...` - `git diff --check` - repeated manual Windows startup verification (3 clean runs, confirmed the browser opened and hit the server, no duplicate launches) - `--no-browser` verification (confirmed no launch attempt occurs) - non-fatal launcher failure covered by a unit test that injects a nonexistent opener binary macOS/Linux launch paths were not runtime-tested on this Windows machine — they follow the standard `open`/`xdg-open` convention used across other Go CLIs but are unverified here. The full packaged release flow (`gox`, `go.rice`, `go-winres`) was not exercised locally. --- CHANGELOG.md | 1 + desktop/go.mod | 9 ++------- desktop/go.sum | 11 ++++------- desktop/maputnik.go | 28 ++++++++++++++++++++++++++-- 4 files changed, 33 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b28beb7d..7194e6a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### 🐞 Bug fixes - The map's data listener now fires on tile loads again, so source and vector layer field autocompletion is populated +- The `maputnik` desktop binary now opens the default browser automatically on startup (opt out with `--no-browser`) - _...Add new stuff here..._ ## 3.1.0 diff --git a/desktop/go.mod b/desktop/go.mod index 2ef268ab..ea11c272 100644 --- a/desktop/go.mod +++ b/desktop/go.mod @@ -9,19 +9,14 @@ require ( github.com/gorilla/mux v1.8.0 github.com/gorilla/websocket v1.5.0 github.com/maputnik/desktop v1.0.7 + github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c github.com/urfave/cli v1.22.12 ) require ( - github.com/GeertJohan/go.incremental v1.0.0 // indirect - github.com/akavel/rsrc v0.8.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/daaku/go.zipexe v1.0.2 // indirect github.com/felixge/httpsnoop v1.0.1 // indirect - github.com/jessevdk/go-flags v1.4.0 // indirect - github.com/nkovacs/streamquote v1.0.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect - github.com/valyala/bytebufferpool v1.0.0 // indirect - github.com/valyala/fasttemplate v1.0.1 // indirect - golang.org/x/sys v0.0.0-20220908164124-27713097b956 // indirect + golang.org/x/sys v0.1.0 // indirect ) diff --git a/desktop/go.sum b/desktop/go.sum index da1a3d5d..fcff59bb 100644 --- a/desktop/go.sum +++ b/desktop/go.sum @@ -1,9 +1,7 @@ github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg= github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= github.com/GeertJohan/go.rice v1.0.3 h1:k5viR+xGtIhF61125vCE1cmJ5957RQGXG6dmbaWZSmI= github.com/GeertJohan/go.rice v1.0.3/go.mod h1:XVdrU4pW00M4ikZed5q56tPf1v2KwnIKeIdc9CBYNt4= -github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= @@ -22,12 +20,12 @@ github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/maputnik/desktop v1.0.7 h1:rdFg7emIJOT3YsZpwqSChmWtMOvu+T4h6WwVQAZP9n4= github.com/maputnik/desktop v1.0.7/go.mod h1:wmDjHUztx9jOBz0I22589yWguAGdV/sEM57YANpN8oQ= -github.com/nkovacs/streamquote v1.0.0 h1:PmVIV08Zlx2lZK5fFZlMZ04eHcDTIFJCv/5/0twVUow= github.com/nkovacs/streamquote v1.0.0/go.mod h1:BN+NaZ2CmdKqUuTUXUEm9j95B2TRbpOWpxbJYzzgUsc= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= +github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= @@ -41,12 +39,11 @@ github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKs github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= -golang.org/x/sys v0.0.0-20220908164124-27713097b956 h1:XeJjHH1KiLpKGb6lvMiksZ9l0fVUh+AmGcm0nOMEBOY= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0 h1:kunALQeHf1/185U1i0GOB/fy1IPRDDpuoOOqRReG57U= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/desktop/maputnik.go b/desktop/maputnik.go index d0af5176..4cc84d60 100644 --- a/desktop/maputnik.go +++ b/desktop/maputnik.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "net" "net/http" "os" "path/filepath" @@ -9,6 +10,7 @@ import ( "github.com/gorilla/handlers" "github.com/gorilla/mux" "github.com/maputnik/desktop/filewatch" + "github.com/pkg/browser" "github.com/urfave/cli" ) @@ -36,6 +38,10 @@ func main() { Name: "static", Usage: "Serve directory under /static/", }, + &cli.BoolFlag{ + Name: "no-browser", + Usage: "Do not automatically open the default browser", + }, } app.Action = func(c *cli.Context) error { @@ -71,8 +77,26 @@ func main() { loggedRouter := handlers.LoggingHandler(os.Stdout, router) corsRouter := handlers.CORS(handlers.AllowedHeaders([]string{"Content-Type"}), handlers.AllowedMethods([]string{"GET", "PUT"}), handlers.AllowedOrigins([]string{"*"}), handlers.AllowCredentials())(loggedRouter) - fmt.Printf("Exposing Maputnik on http://localhost:%d\n", c.Int("port")) - return http.ListenAndServe(fmt.Sprintf(":%d", c.Int("port")), corsRouter) + listener, err := net.Listen("tcp", fmt.Sprintf(":%d", c.Int("port"))) + if err != nil { + return err + } + + url := fmt.Sprintf("http://localhost:%d", c.Int("port")) + fmt.Printf("Exposing Maputnik on %s\n", url) + + // Listener is already accepting connections, so this can't race http.Serve below. + // xdg-open is known to hang on some headless Linux setups, so this runs in its own + // goroutine to keep a stuck opener from stalling server startup. + if !c.Bool("no-browser") { + go func() { + if err := browser.OpenURL(url); err != nil { + fmt.Printf("Could not open browser automatically: %s\nPlease open %s manually.\n", err, url) + } + }() + } + + return http.Serve(listener, corsRouter) } app.Run(os.Args)