├── .github └── workflows │ └── push.yml ├── LICENSE ├── README.md ├── browser.go ├── browser_darwin.go ├── browser_freebsd.go ├── browser_linux.go ├── browser_netbsd.go ├── browser_openbsd.go ├── browser_unsupported.go ├── browser_windows.go ├── example_test.go ├── examples └── Open │ └── main.go ├── go.mod └── go.sum /.github/workflows/push.yml: -------------------------------------------------------------------------------- 1 | on: [push, pull_request] 2 | 3 | name: CI 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | go: [ '1.21' ] 9 | os: [ ubuntu-latest, macos-latest, windows-latest ] 10 | fail-fast: false 11 | 12 | runs-on: ${{ matrix.os }} 13 | name: Test suite 14 | 15 | steps: 16 | - uses: actions/checkout@v4 17 | - name: Setup go 18 | uses: actions/setup-go@v4 19 | with: 20 | go-version: ${{ matrix.go }} 21 | - run: go test -v ./... 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Dave Cheney 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # browser 3 | 4 | Helpers to open URLs, readers, or files in the system default web browser. 5 | 6 | This fork adds: 7 | 8 | - `OpenReader` error wrapping; 9 | - `ErrNotFound` error wrapping on BSD; 10 | - Go 1.21 support. 11 | 12 | ## Usage 13 | 14 | ``` go 15 | import "github.com/cli/browser" 16 | 17 | err = browser.OpenURL(url) 18 | err = browser.OpenFile(path) 19 | err = browser.OpenReader(reader) 20 | ``` 21 | -------------------------------------------------------------------------------- /browser.go: -------------------------------------------------------------------------------- 1 | // Package browser provides helpers to open files, readers, and urls in a browser window. 2 | // 3 | // The choice of which browser is started is entirely client dependent. 4 | package browser 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | "os" 10 | "os/exec" 11 | "path/filepath" 12 | ) 13 | 14 | // Stdout is the io.Writer to which executed commands write standard output. 15 | var Stdout io.Writer = os.Stdout 16 | 17 | // Stderr is the io.Writer to which executed commands write standard error. 18 | var Stderr io.Writer = os.Stderr 19 | 20 | // OpenFile opens new browser window for the file path. 21 | func OpenFile(path string) error { 22 | path, err := filepath.Abs(path) 23 | if err != nil { 24 | return err 25 | } 26 | return OpenURL("file://" + path) 27 | } 28 | 29 | // OpenReader consumes the contents of r and presents the 30 | // results in a new browser window. 31 | func OpenReader(r io.Reader) error { 32 | f, err := os.CreateTemp("", "browser.*.html") 33 | if err != nil { 34 | return fmt.Errorf("browser: could not create temporary file: %w", err) 35 | } 36 | if _, err := io.Copy(f, r); err != nil { 37 | f.Close() 38 | return fmt.Errorf("browser: caching temporary file failed: %w", err) 39 | } 40 | if err := f.Close(); err != nil { 41 | return fmt.Errorf("browser: caching temporary file failed: %w", err) 42 | } 43 | return OpenFile(f.Name()) 44 | } 45 | 46 | // OpenURL opens a new browser window pointing to url. 47 | func OpenURL(url string) error { 48 | return openBrowser(url) 49 | } 50 | 51 | func runCmd(prog string, args ...string) error { 52 | cmd := exec.Command(prog, args...) 53 | cmd.Stdout = Stdout 54 | cmd.Stderr = Stderr 55 | return cmd.Run() 56 | } 57 | -------------------------------------------------------------------------------- /browser_darwin.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | func openBrowser(url string) error { 4 | return runCmd("open", url) 5 | } 6 | -------------------------------------------------------------------------------- /browser_freebsd.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | ) 8 | 9 | func openBrowser(url string) error { 10 | err := runCmd("xdg-open", url) 11 | if errors.Is(err, exec.ErrNotFound) { 12 | return fmt.Errorf("%w - install xdg-utils from ports(8)", err) 13 | } 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /browser_linux.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import ( 4 | "os/exec" 5 | "strings" 6 | ) 7 | 8 | func openBrowser(url string) error { 9 | providers := []string{"xdg-open", "x-www-browser", "www-browser", "wslview"} 10 | 11 | // There are multiple possible providers to open a browser on linux 12 | // One of them is xdg-open, another is x-www-browser, then there's www-browser, etc. 13 | // Look for one that exists and run it 14 | for _, provider := range providers { 15 | if _, err := exec.LookPath(provider); err == nil { 16 | return runCmd(provider, url) 17 | } 18 | } 19 | 20 | return &exec.Error{Name: strings.Join(providers, ","), Err: exec.ErrNotFound} 21 | } 22 | -------------------------------------------------------------------------------- /browser_netbsd.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import ( 4 | "errors" 5 | "os/exec" 6 | ) 7 | 8 | func openBrowser(url string) error { 9 | err := runCmd("xdg-open", url) 10 | if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { 11 | return errors.New("xdg-open: command not found - install xdg-utils from pkgsrc(7)") 12 | } 13 | return err 14 | } 15 | -------------------------------------------------------------------------------- /browser_openbsd.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "os/exec" 7 | ) 8 | 9 | func openBrowser(url string) error { 10 | err := runCmd("xdg-open", url) 11 | if errors.Is(err, exec.ErrNotFound) { 12 | return fmt.Errorf("%w - install xdg-utils from ports(8)", err) 13 | } 14 | return err 15 | } 16 | -------------------------------------------------------------------------------- /browser_unsupported.go: -------------------------------------------------------------------------------- 1 | // +build !linux,!windows,!darwin,!openbsd,!freebsd,!netbsd 2 | 3 | package browser 4 | 5 | import ( 6 | "fmt" 7 | "runtime" 8 | ) 9 | 10 | func openBrowser(url string) error { 11 | return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS) 12 | } 13 | -------------------------------------------------------------------------------- /browser_windows.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import "golang.org/x/sys/windows" 4 | 5 | func openBrowser(url string) error { 6 | return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL) 7 | } 8 | -------------------------------------------------------------------------------- /example_test.go: -------------------------------------------------------------------------------- 1 | package browser 2 | 3 | import "strings" 4 | 5 | func ExampleOpenFile() { 6 | _ = OpenFile("index.html") 7 | } 8 | 9 | func ExampleOpenReader() { 10 | // https://github.com/rust-lang/rust/issues/13871 11 | const quote = `There was a night when winds from unknown spaces 12 | whirled us irresistibly into limitless vacum beyond all thought and entity. 13 | Perceptions of the most maddeningly untransmissible sort thronged upon us; 14 | perceptions of infinity which at the time convulsed us with joy, yet which 15 | are now partly lost to my memory and partly incapable of presentation to others.` 16 | r := strings.NewReader(quote) 17 | _ = OpenReader(r) 18 | } 19 | 20 | func ExampleOpenURL() { 21 | const url = "http://golang.org/" 22 | _ = OpenURL(url) 23 | } 24 | -------------------------------------------------------------------------------- /examples/Open/main.go: -------------------------------------------------------------------------------- 1 | // Open is a simple example of the github.com/pkg/browser package. 2 | // 3 | // Usage: 4 | // 5 | // # Open a file in a browser window 6 | // Open $FILE 7 | // 8 | // # Open a URL in a browser window 9 | // Open $URL 10 | // 11 | // # Open the contents of stdin in a browser window 12 | // cat $SOMEFILE | Open 13 | package main 14 | 15 | import ( 16 | "flag" 17 | "fmt" 18 | "log" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | 23 | "github.com/cli/browser" 24 | ) 25 | 26 | func usage() { 27 | fmt.Fprintf(os.Stderr, "Usage:\n %s { | | -}\n", filepath.Base(os.Args[0])) 28 | flag.PrintDefaults() 29 | } 30 | 31 | func init() { 32 | flag.Usage = usage 33 | flag.Parse() 34 | } 35 | 36 | func check(err error) { 37 | if err != nil { 38 | log.Fatal(err) 39 | } 40 | } 41 | 42 | func main() { 43 | args := flag.Args() 44 | 45 | if len(args) != 1 { 46 | usage() 47 | os.Exit(1) 48 | } 49 | 50 | if args[0] == "-" { 51 | check(browser.OpenReader(os.Stdin)) 52 | return 53 | } 54 | 55 | if strings.HasPrefix(args[0], "http:") || strings.HasPrefix(args[0], "https:") { 56 | check(browser.OpenURL(args[0])) 57 | return 58 | } 59 | 60 | check(browser.OpenFile(args[0])) 61 | } 62 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cli/browser 2 | 3 | go 1.21 4 | 5 | require golang.org/x/sys v0.13.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= 2 | golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 3 | --------------------------------------------------------------------------------