From 2f9b2d9dbf42519dcc005f8330b9bf1b5a3cd3c7 Mon Sep 17 00:00:00 2001 From: Billy Lynch Date: Tue, 27 Jan 2026 11:27:25 -0500 Subject: [PATCH] Add Opener struct This change refactors the code to add an opener struct to allow callers to provide call-specific Stdout/Stderr values for opening without needing to modify the global vars. This allows callers to do things like capture individual Open Stderr errors strings without worrying about other usage of it in the application. This also provides a default implementation that should preserve the existing global var behavior. --- browser.go | 72 +++++++++++++++++++++++++++++++++++------- browser_darwin.go | 4 +-- browser_freebsd.go | 4 +-- browser_linux.go | 4 +-- browser_netbsd.go | 4 +-- browser_openbsd.go | 4 +-- browser_test.go | 19 +++++++++++ browser_unsupported.go | 3 +- browser_windows.go | 2 +- 9 files changed, 93 insertions(+), 23 deletions(-) create mode 100644 browser_test.go diff --git a/browser.go b/browser.go index d7969d7..89a07bf 100644 --- a/browser.go +++ b/browser.go @@ -18,18 +18,50 @@ var Stdout io.Writer = os.Stdout // Stderr is the io.Writer to which executed commands write standard error. var Stderr io.Writer = os.Stderr +// Opener allows customizing browser opening behavior. +type Opener struct { + // Stdout is the io.Writer to which executed commands write standard output. + // If nil, os.Stdout is used. + Stdout io.Writer + + // Stderr is the io.Writer to which executed commands write standard error. + // If nil, os.Stderr is used. + Stderr io.Writer +} + +func (o *Opener) stdout() io.Writer { + if o.Stdout != nil { + return o.Stdout + } + return Stdout +} + +func (o *Opener) stderr() io.Writer { + if o.Stderr != nil { + return o.Stderr + } + return Stderr +} + +func (o *Opener) runCmd(prog string, args ...string) error { + cmd := exec.Command(prog, args...) + cmd.Stdout = o.stdout() + cmd.Stderr = o.stderr() + return cmd.Run() +} + // OpenFile opens new browser window for the file path. -func OpenFile(path string) error { +func (o *Opener) OpenFile(path string) error { path, err := filepath.Abs(path) if err != nil { return err } - return OpenURL("file://" + path) + return o.OpenURL("file://" + path) } // OpenReader consumes the contents of r and presents the // results in a new browser window. -func OpenReader(r io.Reader) error { +func (o *Opener) OpenReader(r io.Reader) error { f, err := ioutil.TempFile("", "browser.*.html") if err != nil { return fmt.Errorf("browser: could not create temporary file: %v", err) @@ -41,17 +73,35 @@ func OpenReader(r io.Reader) error { if err := f.Close(); err != nil { return fmt.Errorf("browser: caching temporary file failed: %v", err) } - return OpenFile(f.Name()) + return o.OpenFile(f.Name()) } // OpenURL opens a new browser window pointing to url. -func OpenURL(url string) error { - return openBrowser(url) +func (o *Opener) OpenURL(url string) error { + return o.openBrowser(url) } -func runCmd(prog string, args ...string) error { - cmd := exec.Command(prog, args...) - cmd.Stdout = Stdout - cmd.Stderr = Stderr - return cmd.Run() +// defaultOpener returns an Opener configured with the package-level Stdout/Stderr. +// This is done as a function to always grab the latest values of Stdout/Stderr. +func defaultOpener() *Opener { + return &Opener{ + Stdout: Stdout, + Stderr: Stderr, + } +} + +// OpenFile opens new browser window for the file path. +func OpenFile(path string) error { + return defaultOpener().OpenFile(path) +} + +// OpenReader consumes the contents of r and presents the +// results in a new browser window. +func OpenReader(r io.Reader) error { + return defaultOpener().OpenReader(r) +} + +// OpenURL opens a new browser window pointing to url. +func OpenURL(url string) error { + return defaultOpener().OpenURL(url) } diff --git a/browser_darwin.go b/browser_darwin.go index 8507cf7..2ea95f2 100644 --- a/browser_darwin.go +++ b/browser_darwin.go @@ -1,5 +1,5 @@ package browser -func openBrowser(url string) error { - return runCmd("open", url) +func (o *Opener) openBrowser(url string) error { + return o.runCmd("open", url) } diff --git a/browser_freebsd.go b/browser_freebsd.go index 4fc7ff0..48b7f62 100644 --- a/browser_freebsd.go +++ b/browser_freebsd.go @@ -5,8 +5,8 @@ import ( "os/exec" ) -func openBrowser(url string) error { - err := runCmd("xdg-open", url) +func (o *Opener) openBrowser(url string) error { + err := o.runCmd("xdg-open", url) if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { return errors.New("xdg-open: command not found - install xdg-utils from ports(8)") } diff --git a/browser_linux.go b/browser_linux.go index d26cddd..42f3b6f 100644 --- a/browser_linux.go +++ b/browser_linux.go @@ -5,7 +5,7 @@ import ( "strings" ) -func openBrowser(url string) error { +func (o *Opener) openBrowser(url string) error { providers := []string{"xdg-open", "x-www-browser", "www-browser"} // There are multiple possible providers to open a browser on linux @@ -13,7 +13,7 @@ func openBrowser(url string) error { // Look for one that exists and run it for _, provider := range providers { if _, err := exec.LookPath(provider); err == nil { - return runCmd(provider, url) + return o.runCmd(provider, url) } } diff --git a/browser_netbsd.go b/browser_netbsd.go index 65a5e5a..db5c9a9 100644 --- a/browser_netbsd.go +++ b/browser_netbsd.go @@ -5,8 +5,8 @@ import ( "os/exec" ) -func openBrowser(url string) error { - err := runCmd("xdg-open", url) +func (o *Opener) openBrowser(url string) error { + err := o.runCmd("xdg-open", url) if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { return errors.New("xdg-open: command not found - install xdg-utils from pkgsrc(7)") } diff --git a/browser_openbsd.go b/browser_openbsd.go index 4fc7ff0..48b7f62 100644 --- a/browser_openbsd.go +++ b/browser_openbsd.go @@ -5,8 +5,8 @@ import ( "os/exec" ) -func openBrowser(url string) error { - err := runCmd("xdg-open", url) +func (o *Opener) openBrowser(url string) error { + err := o.runCmd("xdg-open", url) if e, ok := err.(*exec.Error); ok && e.Err == exec.ErrNotFound { return errors.New("xdg-open: command not found - install xdg-utils from ports(8)") } diff --git a/browser_test.go b/browser_test.go new file mode 100644 index 0000000..6ffbadf --- /dev/null +++ b/browser_test.go @@ -0,0 +1,19 @@ +package browser + +import ( + "bytes" + "path/filepath" + "testing" +) + +func TestErrorRedirect(t *testing.T) { + stderr := new(bytes.Buffer) + o := &Opener{ + Stderr: stderr, + } + + _ = o.OpenFile(filepath.Join(t.TempDir(), "nonexistentfile.html")) + if stderr.Len() == 0 { + t.Errorf("expected stderr to contain error message, got empty") + } +} diff --git a/browser_unsupported.go b/browser_unsupported.go index 7c5c17d..29d9a9f 100644 --- a/browser_unsupported.go +++ b/browser_unsupported.go @@ -1,3 +1,4 @@ +//go:build !linux && !windows && !darwin && !openbsd && !freebsd && !netbsd // +build !linux,!windows,!darwin,!openbsd,!freebsd,!netbsd package browser @@ -7,6 +8,6 @@ import ( "runtime" ) -func openBrowser(url string) error { +func (o *Opener) openBrowser(url string) error { return fmt.Errorf("openBrowser: unsupported operating system: %v", runtime.GOOS) } diff --git a/browser_windows.go b/browser_windows.go index 63e1929..eac213f 100644 --- a/browser_windows.go +++ b/browser_windows.go @@ -2,6 +2,6 @@ package browser import "golang.org/x/sys/windows" -func openBrowser(url string) error { +func (o *Opener) openBrowser(url string) error { return windows.ShellExecute(0, nil, windows.StringToUTF16Ptr(url), nil, nil, windows.SW_SHOWNORMAL) }