Skip to content

Commit

Permalink
Add openApp method (#263)
Browse files Browse the repository at this point in the history
Co-authored-by: Sindre Sorhus <sindresorhus@gmail.com>
  • Loading branch information
Kudo and sindresorhus committed Oct 7, 2021
1 parent e21e623 commit 1acc682
Show file tree
Hide file tree
Showing 4 changed files with 120 additions and 12 deletions.
33 changes: 33 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,15 @@ declare namespace open {
readonly allowNonzeroExitCode?: boolean;
}

interface OpenAppOptions extends Omit<Options, 'app'> {
/**
Arguments passed to the app.
These arguments are app dependent. Check the app's documentation for what arguments it accepts.
*/
readonly arguments?: readonly string[];
}

type AppName =
| 'chrome'
| 'firefox'
Expand Down Expand Up @@ -115,6 +124,30 @@ declare const open: {
```
*/
apps: Record<open.AppName, string | readonly string[]>;

/**
Open an app. Cross-platform.
Uses the command `open` on macOS, `start` on Windows and `xdg-open` on other platforms.
@param name - The app you want to open. Can be either builtin supported `open.apps` names or other name supported in platform.
@returns The [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process.
@example
```
const {apps, openApp} = require('open');
// Open Firefox
await openApp(apps.firefox);
// Open Chrome incognito mode
await openApp(apps.chrome, {arguments: ['--incognito']});
// Open Xcode
await openApp('xcode');
```
*/
openApp: (name: open.App['name'], options?: open.OpenAppOptions) => Promise<ChildProcess>;
};

export = open;
55 changes: 43 additions & 12 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,11 +69,7 @@ const pTryEach = async (array, mapper) => {
throw latestError;
};

const open = async (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}

const baseOpen = async options => {
options = {
wait: false,
background: false,
Expand All @@ -83,7 +79,7 @@ const open = async (target, options) => {
};

if (Array.isArray(options.app)) {
return pTryEach(options.app, singleApp => open(target, {
return pTryEach(options.app, singleApp => baseOpen({
...options,
app: singleApp
}));
Expand All @@ -93,7 +89,7 @@ const open = async (target, options) => {
appArguments = [...appArguments];

if (Array.isArray(app)) {
return pTryEach(app, appName => open(target, {
return pTryEach(app, appName => baseOpen({
...options,
app: {
name: appName,
Expand Down Expand Up @@ -153,9 +149,11 @@ const open = async (target, options) => {
// Double quote with double quotes to ensure the inner quotes are passed through.
// Inner quotes are delimited for PowerShell interpretation with backticks.
encodedArguments.push(`"\`"${app}\`""`, '-ArgumentList');
appArguments.unshift(target);
} else {
encodedArguments.push(`"${target}"`);
if (options.target) {
appArguments.unshift(options.target);
}
} else if (options.target) {
encodedArguments.push(`"${options.target}"`);
}

if (appArguments.length > 0) {
Expand All @@ -164,7 +162,7 @@ const open = async (target, options) => {
}

// Using Base64-encoded command, accepted by PowerShell, to allow special characters.
target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
options.target = Buffer.from(encodedArguments.join(' '), 'utf16le').toString('base64');
} else {
if (app) {
command = app;
Expand Down Expand Up @@ -196,7 +194,9 @@ const open = async (target, options) => {
}
}

cliArguments.push(target);
if (options.target) {
cliArguments.push(options.target);
}

if (platform === 'darwin' && appArguments.length > 0) {
cliArguments.push('--args', ...appArguments);
Expand Down Expand Up @@ -224,6 +224,36 @@ const open = async (target, options) => {
return subprocess;
};

const open = (target, options) => {
if (typeof target !== 'string') {
throw new TypeError('Expected a `target`');
}

return baseOpen({
...options,
target
});
};

const openApp = (name, options) => {
if (typeof name !== 'string') {
throw new TypeError('Expected a `name`');
}

const {arguments: appArguments = []} = options || {};
if (appArguments !== undefined && appArguments !== null && !Array.isArray(appArguments)) {
throw new TypeError('Expected `appArguments` as Array type');
}

return baseOpen({
...options,
app: {
name,
arguments: appArguments
}
});
};

function detectArchBinary(binary) {
if (typeof binary === 'string' || Array.isArray(binary)) {
return binary;
Expand Down Expand Up @@ -280,5 +310,6 @@ defineLazyProperty(apps, 'edge', () => detectPlatformBinary({
}));

open.apps = apps;
open.openApp = openApp;

module.exports = open;
35 changes: 35 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,12 @@ await open('https://sindresorhus.com', {app: {name: 'firefox'}});

// Specify app arguments.
await open('https://sindresorhus.com', {app: {name: 'google chrome', arguments: ['--incognito']}});

// Open an app
await open.openApp('xcode');

// Open an app with arguments
await open.openApp(open.apps.chrome, {arguments: ['--incognito']});
```

## API
Expand Down Expand Up @@ -130,6 +136,35 @@ await open('https://google.com', {
- [`firefox`](https://www.mozilla.org/firefox) - Web browser
- [`edge`](https://www.microsoft.com/edge) - Web browser

### open.openApp(name, options?)

Open an app.

Returns a promise for the [spawned child process](https://nodejs.org/api/child_process.html#child_process_class_childprocess). You would normally not need to use this for anything, but it can be useful if you'd like to attach custom event listeners or perform other operations directly on the spawned process.

#### name

Type: `string`

The app name is platform dependent. Don't hard code it in reusable modules. For example, Chrome is `google chrome` on macOS, `google-chrome` on Linux and `chrome` on Windows. If possible, use [`open.apps`](#openapps) which auto-detects the correct binary to use.

You may also pass in the app's full path. For example on WSL, this can be `/mnt/c/Program Files (x86)/Google/Chrome/Application/chrome.exe` for the Windows installation of Chrome.

#### options

Type: `object`

Same options as [`open`](#options) except `app` and with the following additions:

##### arguments

Type: `string[]`\
Default: `[]`

Arguments passed to the app.

These arguments are app dependent. Check the app's documentation for what arguments it accepts.

## Related

- [open-cli](https://github.com/sindresorhus/open-cli) - CLI for this module
Expand Down
9 changes: 9 additions & 0 deletions test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const test = require('ava');
const open = require('.');
const {openApp} = open;

// Tests only checks that opening doesn't return an error
// it has no way make sure that it actually opened anything.
Expand Down Expand Up @@ -69,3 +70,11 @@ test('open URL with query strings and URL reserved characters', async t => {
test('open URL with query strings and URL reserved characters with `url` option', async t => {
await t.notThrowsAsync(open('https://httpbin.org/get?amp=%26&colon=%3A&comma=%2C&commat=%40&dollar=%24&equals=%3D&plus=%2B&quest=%3F&semi=%3B&sol=%2F', {url: true}));
});

test('open Firefox without arguments', async t => {
await t.notThrowsAsync(openApp(open.apps.firefox));
});

test('open Chrome in incognito mode', async t => {
await t.notThrowsAsync(openApp(open.apps.chrome, {arguments: ['--incognito'], newInstance: true}));
});

0 comments on commit 1acc682

Please sign in to comment.