Skip to content

Commit

Permalink
fix: open URL in browser on WSL (#128)
Browse files Browse the repository at this point in the history
Here's a more elegant solution for opening URLs from WSL in your
favorite browser. It is based on `sensible-browser` which is included in
the default distribution for WSL. This avoids issues with the WSL
environment, `cmd.exe`. quoting parameters, etc.

In WSL, set the default browser using the `BROWSER` variable, for
example,

```sh
export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
or
export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
```

Note: To permanently set the default browser, add the appropriate entry
to your shell's RC file, e.g. .bashrc or .zshrc.

To launch a URL from the WSL command line:

```sh
sensible-browser https://google.com
```

To launch a URL using `promise-spawn`:

```js
const promiseSpawn = require('@npmcli/promise-spawn')
promiseSpawn.open('https://google.com')
```

Replaces #118
Closes #62

### Test

```
os: 5.15.153.1-microsoft-standard-WSL2
node: 20.18.0
npm: 10.8.2
```


![image](https://github.com/user-attachments/assets/899801c5-6f05-477e-92c7-f2669526fa03)

---------

Co-authored-by: Gar <wraithgar@github.com>
  • Loading branch information
mbtools and wraithgar authored Oct 18, 2024
1 parent 6b0e577 commit 5ecf301
Show file tree
Hide file tree
Showing 2 changed files with 59 additions and 24 deletions.
16 changes: 14 additions & 2 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,19 @@ const open = (_args, opts = {}, extra = {}) => {

let platform = process.platform
// process.platform === 'linux' may actually indicate WSL, if that's the case
// we want to treat things as win32 anyway so the host can open the argument
// open the argument with sensible-browser which is pre-installed
// In WSL, set the default browser using, for example,
// export BROWSER="/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe"
// or
// export BROWSER="/mnt/c/Program Files (x86)/Microsoft/Edge/Application/msedge.exe"
// To permanently set the default browser, add the appropriate entry to your shell's
// RC file, e.g. .bashrc or .zshrc.
if (platform === 'linux' && os.release().toLowerCase().includes('microsoft')) {
platform = 'win32'
platform = 'wsl'
if (!process.env.BROWSER) {
return Promise.reject(
new Error('Set the BROWSER environment variable to your desired browser.'))
}
}

let command = options.command
Expand All @@ -146,6 +156,8 @@ const open = (_args, opts = {}, extra = {}) => {
// accidentally interpret the first arg as the title, we stick an empty
// string immediately after the start command
command = 'start ""'
} else if (platform === 'wsl') {
command = 'sensible-browser'
} else if (platform === 'darwin') {
command = 'open'
} else {
Expand Down
67 changes: 45 additions & 22 deletions test/open.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

const spawk = require('spawk')
const t = require('tap')
const os = require('node:os')

const promiseSpawn = require('../lib/index.js')

Expand All @@ -10,6 +11,8 @@ t.afterEach(() => {
spawk.clean()
})

const isWSL = process.platform === 'linux' && os.release().toLowerCase().includes('microsoft')

t.test('process.platform === win32', (t) => {
const comSpec = process.env.ComSpec
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
Expand Down Expand Up @@ -118,7 +121,8 @@ t.test('process.platform === linux', (t) => {
Object.defineProperty(process, 'platform', platformDesc)
})

t.test('uses xdg-open in a shell', async (t) => {
// xdg-open is not installed in WSL by default
t.test('uses xdg-open in a shell', { skip: isWSL }, async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com')
Expand All @@ -130,7 +134,8 @@ t.test('process.platform === linux', (t) => {
t.ok(proc.called)
})

t.test('ignores shell = false', async (t) => {
// xdg-open is not installed in WSL by default
t.test('ignores shell = false', { skip: isWSL }, async (t) => {
const proc = spawk.spawn('sh', ['-c', 'xdg-open https://google.com'], { shell: false })

const result = await promiseSpawn.open('https://google.com', { shell: false })
Expand All @@ -154,58 +159,76 @@ t.test('process.platform === linux', (t) => {
t.ok(proc.called)
})

t.test('when os.release() includes Microsoft treats as win32', async (t) => {
const comSpec = process.env.ComSpec
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
t.teardown(() => {
process.env.ComSPec = comSpec
})

t.test('when os.release() includes Microsoft treats as WSL', async (t) => {
const promiseSpawnMock = t.mock('../lib/index.js', {
os: {
release: () => 'Microsoft',
},
})
const browser = process.env.BROWSER
process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'

const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'start "" https://google.com'],
{ shell: false })
const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false })

const result = await promiseSpawnMock.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: undefined,
})

t.ok(proc.called)
})

t.test('when os.release() includes microsoft treats as win32', async (t) => {
const comSpec = process.env.ComSpec
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
t.teardown(() => {
process.env.ComSPec = comSpec
process.env.BROWSER = browser
})

t.ok(proc.called)
})

t.test('when os.release() includes microsoft treats as WSL', async (t) => {
const promiseSpawnMock = t.mock('../lib/index.js', {
os: {
release: () => 'microsoft',
},
})
const browser = process.env.BROWSER
process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'

const proc = spawk.spawn('C:\\Windows\\System32\\cmd.exe',
['/d', '/s', '/c', 'start "" https://google.com'],
{ shell: false })
const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false })

const result = await promiseSpawnMock.open('https://google.com')
t.hasStrict(result, {
code: 0,
signal: undefined,
})

t.teardown(() => {
process.env.BROWSER = browser
})

t.ok(proc.called)
})

t.test('fails on WSL if BROWSER is not set', async (t) => {
const promiseSpawnMock = t.mock('../lib/index.js', {
os: {
release: () => 'microsoft',
},
})
const browser = process.env.BROWSER
delete process.env.BROWSER

const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false })

await t.rejects(promiseSpawnMock.open('https://google.com'), {
message: 'Set the BROWSER environment variable to your desired browser.',
})

t.teardown(() => {
process.env.BROWSER = browser
})

t.notOk(proc.called)
})

t.end()
})

Expand Down

0 comments on commit 5ecf301

Please sign in to comment.