Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions browser/browse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package browser

import "os/exec"

// browse starts the named program with the given arguments
// and waits for it to complete.
var browse = func(name string, args ...string) error {
// #nosec G204
// executable names are hardcoded by this package
// and never originate from user input.
cmd := exec.Command(name, args...)
cmd.Stdin = nil
cmd.Stdout = nil
cmd.Stderr = nil

return cmd.Run()
}
51 changes: 51 additions & 0 deletions browser/browse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package browser

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

func TestRun(t *testing.T) {
tests := []struct {
name string
command string
args []string
wantErr bool
}{
{
name: "valid_command",
command: "echo",
args: []string{"hello"},
wantErr: false,
},
{
name: "invalid_command",
command: "nonexistent-command-xyz",
args: nil,
wantErr: true,
},
{
name: "command_with_empty_args",
command: "echo",
args: []string{},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
if runtime.GOOS == "windows" && tt.command == "echo" {
t.Skip("echo is not a standalone executable on Windows")
}
err := browse(tt.command, tt.args...)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
50 changes: 50 additions & 0 deletions browser/open.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package browser

import (
"fmt"
"os"
"runtime"
)

// Open opens the given URL in the system browser.
// It detects the platform and chooses the appropriate command.
//
// On macOS and Windows it checks for remote/SSH sessions first.
// On Linux (including WSL) it checks for an active display server.
// Returns an error if no display server is available.
func Open(url string) error {
switch runtime.GOOS {
case "darwin":
return browse("open", url)
case "windows":
return browse("explorer.exe", url)
case "linux":
return openOnLinux(url)
default:
return fmt.Errorf("unsupported platform: %s", runtime.GOOS)
}
}

// openOnLinux handles browser opening on Linux and WSL.
func openOnLinux(url string) error {
if runningOnWSL() {
return openOnWSL(url)
}

if !hasDisplay() {
return fmt.Errorf("no display server detected")
}

return browse("xdg-open", url)
}

// openOnWSL handles browser opening inside Windows Subsystem for Linux.
func openOnWSL(url string) error {
return browse("explorer.exe", url)
}

// hasDisplay checks whether a display server is available
// by testing the DISPLAY and WAYLAND_DISPLAY environment variables.
func hasDisplay() bool {
return os.Getenv("DISPLAY") != "" || os.Getenv("WAYLAND_DISPLAY") != ""
}
177 changes: 177 additions & 0 deletions browser/open_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package browser

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

type call struct {
name string
args []string
}

func TestOpenOnLinux(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("openOnLinux only runs on Linux")
}
if runningOnWSL() {
t.Skip("WSL routes to explorer.exe, tested separately")
}

tests := []struct {
name string
display string
wayland string
wantErr bool
wantCalls []call
}{
{
name: "with display",
display: ":0",
wantCalls: []call{
{name: "xdg-open", args: []string{"https://example.com"}},
},
},
{
name: "no display",
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("DISPLAY", tt.display)
t.Setenv("WAYLAND_DISPLAY", tt.wayland)

var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := openOnLinux("https://example.com")
if tt.wantErr {
assert.Error(t, err)
assert.Contains(t, err.Error(), "no display server detected")
assert.Empty(t, calls, "browse should not be called on error")
return
}

assert.NoError(t, err)
assert.Equal(t, tt.wantCalls, calls)
})
}
}

func TestOpenOnWSL(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("openOnWSL only runs on Linux")
}
if !runningOnWSL() {
t.Skip("not running on WSL")
}

var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := openOnWSL("https://example.com")
assert.NoError(t, err)
assert.Equal(t, []call{
{name: "explorer.exe", args: []string{"https://example.com"}},
}, calls)
}

func TestOpen(t *testing.T) {
t.Setenv("DISPLAY", "")
t.Setenv("WAYLAND_DISPLAY", "")

switch runtime.GOOS {
case "linux":
if runningOnWSL() {
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{
{
name: "explorer.exe",
args: []string{"https://example.com"},
},
}, calls,
)
} else {
err := Open("https://example.com")
assert.Error(t, err)
assert.Contains(t, err.Error(), "no display server detected")
}
case "darwin":
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{{name: "open", args: []string{"https://example.com"}}},
calls,
)
case "windows":
var calls []call
mockBrowse(t, func(name string, args ...string) error {
calls = append(calls, call{name, args})
return nil
})

err := Open("https://example.com")
assert.NoError(t, err)
assert.Equal(
t,
[]call{{name: "explorer.exe", args: []string{"https://example.com"}}},
calls,
)
}
}

func TestHasDisplay(t *testing.T) {
tests := []struct {
name string
display string
wayland string
want bool
}{
{name: "neither set", display: "", wayland: "", want: false},
{name: "x11 only", display: ":0", wayland: "", want: true},
{name: "wayland only", display: "", wayland: "wayland-0", want: true},
{name: "both set", display: ":0", wayland: "wayland-0", want: true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Setenv("DISPLAY", tt.display)
t.Setenv("WAYLAND_DISPLAY", tt.wayland)
assert.Equal(t, tt.want, hasDisplay())
})
}
}

// mockBrowse replaces the package-level browse function for the duration of t.
func mockBrowse(t *testing.T, fn func(string, ...string) error) {
t.Helper()
oldBrowse := browse
browse = fn
t.Cleanup(func() { browse = oldBrowse })
}
29 changes: 29 additions & 0 deletions browser/wsl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package browser

import (
"os"
"strings"
)

// runningOnWSL reports whether the current process is running
// inside Windows Subsystem for Linux (WSL).
func runningOnWSL() bool {
return isWSL(os.ReadFile)
}

// isWSL reports whether the system is running inside Windows Subsystem for Linux (WSL)
// by inspecting the Linux kernel version information.
//
// The readFile function is injected to allow the detection logic to be
// tested without reading from the real filesystem.
func isWSL(readFile func(string) ([]byte, error)) bool {
data, err := readFile("/proc/version")
if err != nil {
return false
}

return strings.Contains(
strings.ToLower(string(data)),
"microsoft",
)
}
70 changes: 70 additions & 0 deletions browser/wsl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package browser

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
)

func TestIsWSL(t *testing.T) {
tests := []struct {
name string
content string
want bool
}{
{
name: "wsl2_marker",
content: "Linux version 5.10.16.3-microsoft-standard-WSL2",
want: true,
},
{
name: "microsoft_mixed_case",
content: "Linux version 5.15.0 (Microsoft@Microsoft.com) (gcc 11.2.0) #1 SMP",
want: true,
},
{
name: "no_microsoft_marker",
content: "Linux version 6.1.0-23-amd64 (debian-kernel@lists.debian.org)",
want: false,
},
{
name: "empty_content",
content: "",
want: false,
},
{
name: "partial_word_micah",
content: "Linux version 5.15.0 (micah@kernel.org)",
want: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
reader := func(_ string) ([]byte, error) {
return []byte(tt.content), nil
}
assert.Equal(t, tt.want, isWSL(reader))
})
}
}

func TestIsWSLReadError(t *testing.T) {
t.Parallel()

reader := func(_ string) ([]byte, error) {
return nil, fmt.Errorf("permission denied")
}

assert.False(t, isWSL(reader))
}

func TestRunningOnWSL(t *testing.T) {
t.Parallel()

// smoke test: verify runningOnWSL doesn't panic and returns a bool.
result := runningOnWSL()
assert.IsType(t, false, result)
}
Loading