Skip to content

Commit 5ecf301

Browse files
mbtoolswraithgar
andauthored
fix: open URL in browser on WSL (#128)
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 <[email protected]>
1 parent 6b0e577 commit 5ecf301

File tree

2 files changed

+59
-24
lines changed

2 files changed

+59
-24
lines changed

lib/index.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -131,9 +131,19 @@ const open = (_args, opts = {}, extra = {}) => {
131131

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

139149
let command = options.command
@@ -146,6 +156,8 @@ const open = (_args, opts = {}, extra = {}) => {
146156
// accidentally interpret the first arg as the title, we stick an empty
147157
// string immediately after the start command
148158
command = 'start ""'
159+
} else if (platform === 'wsl') {
160+
command = 'sensible-browser'
149161
} else if (platform === 'darwin') {
150162
command = 'open'
151163
} else {

test/open.js

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const spawk = require('spawk')
44
const t = require('tap')
5+
const os = require('node:os')
56

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

@@ -10,6 +11,8 @@ t.afterEach(() => {
1011
spawk.clean()
1112
})
1213

14+
const isWSL = process.platform === 'linux' && os.release().toLowerCase().includes('microsoft')
15+
1316
t.test('process.platform === win32', (t) => {
1417
const comSpec = process.env.ComSpec
1518
const platformDesc = Object.getOwnPropertyDescriptor(process, 'platform')
@@ -118,7 +121,8 @@ t.test('process.platform === linux', (t) => {
118121
Object.defineProperty(process, 'platform', platformDesc)
119122
})
120123

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

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

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

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

157-
t.test('when os.release() includes Microsoft treats as win32', async (t) => {
158-
const comSpec = process.env.ComSpec
159-
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
160-
t.teardown(() => {
161-
process.env.ComSPec = comSpec
162-
})
163-
162+
t.test('when os.release() includes Microsoft treats as WSL', async (t) => {
164163
const promiseSpawnMock = t.mock('../lib/index.js', {
165164
os: {
166165
release: () => 'Microsoft',
167166
},
168167
})
168+
const browser = process.env.BROWSER
169+
process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'
169170

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

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

180-
t.ok(proc.called)
181-
})
182-
183-
t.test('when os.release() includes microsoft treats as win32', async (t) => {
184-
const comSpec = process.env.ComSpec
185-
process.env.ComSpec = 'C:\\Windows\\System32\\cmd.exe'
186179
t.teardown(() => {
187-
process.env.ComSPec = comSpec
180+
process.env.BROWSER = browser
188181
})
189182

183+
t.ok(proc.called)
184+
})
185+
186+
t.test('when os.release() includes microsoft treats as WSL', async (t) => {
190187
const promiseSpawnMock = t.mock('../lib/index.js', {
191188
os: {
192189
release: () => 'microsoft',
193190
},
194191
})
192+
const browser = process.env.BROWSER
193+
process.env.BROWSER = '/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe'
195194

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

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

203+
t.teardown(() => {
204+
process.env.BROWSER = browser
205+
})
206+
206207
t.ok(proc.called)
207208
})
208209

210+
t.test('fails on WSL if BROWSER is not set', async (t) => {
211+
const promiseSpawnMock = t.mock('../lib/index.js', {
212+
os: {
213+
release: () => 'microsoft',
214+
},
215+
})
216+
const browser = process.env.BROWSER
217+
delete process.env.BROWSER
218+
219+
const proc = spawk.spawn('sh', ['-c', 'sensible-browser https://google.com'], { shell: false })
220+
221+
await t.rejects(promiseSpawnMock.open('https://google.com'), {
222+
message: 'Set the BROWSER environment variable to your desired browser.',
223+
})
224+
225+
t.teardown(() => {
226+
process.env.BROWSER = browser
227+
})
228+
229+
t.notOk(proc.called)
230+
})
231+
209232
t.end()
210233
})
211234

0 commit comments

Comments
 (0)