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
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

### Unreleased

Breaking changes:

- Updates the bundled `terminal-notifier` to the official 3.1.0 release, a universal binary that runs natively on Apple silicon (no Rosetta) as well as Intel. Fixes [#361](https://github.com/mikaelbr/node-notifier/issues/361). The bundle is downloaded from the upstream GitHub release and checked against a recorded SHA-256 by `scripts/update-terminal-notifier.sh`.
- The minimum macOS version for native notifications is now 10.14 (Mojave); older versions fall back to Growl as before.
- macOS asks for notification permission once, the first time a notification is sent. node-notifier registers the bundled app with LaunchServices on first use so that prompt can appear.
- `icon`/`appIcon`, `sender`, `closeLabel` and `dropdownLabel` have no effect on macOS anymore: the `UserNotifications` framework does not support them.
- On macOS, `wait`/`timeout` only wait for a response when `actions` or `reply` is set.

Fixes:

- `actions` are passed as separate `-action` flags and `reply` accepts a placeholder string, matching terminal-notifier 3.
- `utils.isMountainLion()` is deprecated in favour of `utils.isMojaveOrLater()` (kept as an alias).

### `v10.0.1`

Fixes:
Expand Down
36 changes: 23 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ notifier.notify({

## Requirements

- **macOS**: >= 10.8 for native notifications, or Growl if earlier.
- **macOS**: >= 10.14 for native notifications, or Growl if earlier. Ships a universal (Intel + Apple silicon) `terminal-notifier`, so Rosetta is not needed.
- **Linux**: `notify-osd` or `libnotify-bin` installed (Ubuntu should have this by default)
- **Windows**: >= 8, or task bar balloons for Windows < 8. Growl as fallback. Growl takes precedence over Windows balloons.
- **General Fallback**: Growl
Expand Down Expand Up @@ -134,10 +134,17 @@ new nn.Growl(options).notify(options);

Same usage and parameter setup as [**`terminal-notifier`**](https://github.com/julienXX/terminal-notifier).

Native Notification Center requires macOS version 10.8 or higher. If you have
Native Notification Center requires macOS version 10.14 or higher. If you have
an earlier version, Growl will be the fallback. If Growl isn't installed, an
error will be returned in the callback.

The bundled [`terminal-notifier`](https://github.com/julienXX/terminal-notifier)
is the official universal release (Intel and Apple silicon), built on Apple's
`UserNotifications` framework. The first notification on a machine makes macOS
ask for permission once, under the name **terminal-notifier** in System
Settings → Notifications. If that permission is declined, notifications fail
with an error in the callback until it is granted again there.

#### Example

Because `node-notifier` wraps around [**`terminal-notifier`**](https://github.com/julienXX/terminal-notifier),
Expand All @@ -158,7 +165,7 @@ but they aren't documented.
const NotificationCenter = require('node-notifier').NotificationCenter;

var notifier = new NotificationCenter({
withFallback: false, // Use Growl Fallback if <= 10.8
withFallback: false, // Use Growl Fallback if < 10.14
customPath: undefined // Relative/Absolute path to binary if you want to use your own fork of terminal-notifier
});

Expand All @@ -168,17 +175,17 @@ notifier.notify(
subtitle: undefined,
message: undefined,
sound: false, // Case Sensitive string for location of sound file, or use one of macOS' native sounds (see below)
icon: 'Terminal Icon', // Absolute Path to Triggering Icon
contentImage: undefined, // Absolute Path to Attached Image (Content Image)
icon: undefined, // Ignored on macOS: the icon always comes from the sending app bundle (see below)
contentImage: undefined, // Absolute Path to Attached Image (Content Image). Local files only.
open: undefined, // URL to open on Click
wait: false, // Wait for User Action against Notification or times out. Same as timeout = 5 seconds

// New in latest version. See `example/macInput.js` for usage
timeout: 5, // Takes precedence over wait if both are defined.
closeLabel: undefined, // String. Label for cancel button
actions: undefined, // String | Array<String>. Action label or list of labels in case of dropdown
dropdownLabel: undefined, // String. Label to be used if multiple actions
reply: false // Boolean. If notification should take input. Value passed as third argument in callback and event emitter.
// See `example/macInput.js` for usage
timeout: 5, // Takes precedence over wait if both are defined. Only waits when `actions` or `reply` is set.
closeLabel: undefined, // Ignored on macOS >= 10.14 (no equivalent in the UserNotifications framework)
actions: undefined, // String | Array<String>. One button per label.
dropdownLabel: undefined, // Ignored on macOS >= 10.14 (no equivalent in the UserNotifications framework)
reply: false // Boolean | String. If notification should take input; a string is used as placeholder. Value passed as third argument in callback and event emitter.
},
function (error, response, metadata) {
console.log(response, metadata);
Expand All @@ -198,7 +205,9 @@ high value, or to nothing at all.

---

**For macOS notifications: `icon`, `contentImage`, and all forms of `reply`/`actions` require macOS 10.9.**
**For macOS notifications:** `terminal-notifier` only waits for a response (and
therefore only reports `click`, `replied` and `timeout`) when `actions` or
`reply` is set. A plain notification returns as soon as it is delivered.

Sound can be one of these: `Basso`, `Blow`, `Bottle`, `Frog`, `Funk`, `Glass`,
`Hero`, `Morse`, `Ping`, `Pop`, `Purr`, `Sosumi`, `Submarine`, `Tink`.
Expand Down Expand Up @@ -416,7 +425,8 @@ parent application initiating the notification. For `node-notifier`, `terminal-n
is the initiator, and it has the Terminal icon defined as its icon.

To define your custom icon, you need to fork `terminal-notifier` and build your
custom version with your icon.
custom version with your icon, then point `customPath` at it. The `icon` option
is accepted for compatibility but has no effect on macOS.

See [Issue #71 for more info](https://github.com/mikaelbr/node-notifier/issues/71)
<https://github.com/mikaelbr/node-notifier/issues/71>.
Expand Down
110 changes: 98 additions & 12 deletions lib/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,14 +56,13 @@ module.exports.command = function (notifier, options, cb) {
console.info('[notifier options]', options.join(' '));
}

return cp.exec(notifier + ' ' + options.join(' '), function (
error,
stdout,
stderr
) {
if (error) return cb(error);
cb(stderr, stdout);
});
return cp.exec(
notifier + ' ' + options.join(' '),
function (error, stdout, stderr) {
if (error) return cb(error);
cb(stderr, stdout);
}
);
};

module.exports.fileCommand = function (notifier, options, cb) {
Expand All @@ -86,18 +85,97 @@ module.exports.fileCommandJson = function (notifier, options, cb) {
console.info('[notifier options]', options.join(' '));
}
return cp.execFile(notifier, options, function (error, stdout, stderr) {
// terminal-notifier >= 3 exits with 6 when -timeout expires. That is a
// regular outcome for node-notifier, not an error.
if (error && error.code === MAC_EXIT_NO_RESPONSE) {
error = null;
}
if (error) return cb(error, stdout);
if (!stdout) return cb(error, {});

try {
const data = JSON.parse(stdout);
const data = parseMacResponse(options, stdout);
cb(!stderr ? null : stderr, data);
} catch (e) {
cb(e, stdout);
}
});
};

// Exit codes of terminal-notifier >= 3.
const MAC_EXIT_NOT_AUTHORIZED = 3;
const MAC_EXIT_NO_RESPONSE = 6;
module.exports.MAC_EXIT_NOT_AUTHORIZED = MAC_EXIT_NOT_AUTHORIZED;
module.exports.MAC_EXIT_NO_RESPONSE = MAC_EXIT_NO_RESPONSE;

/**
* Normalises what terminal-notifier prints on stdout into the object shape
* node-notifier has always handed to callbacks and event listeners:
* `{ activationType, activationValue }`.
*
* terminal-notifier 1.x/2.x (and forks used through `customPath`) print JSON
* when passed `-json`. terminal-notifier >= 3 prints a single line: the
* clicked action title, the typed reply, or one of the markers `@TIMEOUT`,
* `@CLOSED`, `@ACTIONCLICKED`. `-list` and `-remove` print free text.
*/
function parseMacResponse(argsList, stdout) {
argsList = argsList || [];
try {
return JSON.parse(stdout);
} catch (e) {
// Not JSON; fall through to the plain-text protocol of terminal-notifier 3.
}

if (inArray(argsList, '-list') || inArray(argsList, '-remove')) {
return stdout;
}

const text = String(stdout).replace(/\r?\n$/, '');
switch (text) {
case '@TIMEOUT':
return { activationType: 'timedOut' };
case '@CLOSED':
return { activationType: 'closed' };
case '@ACTIONCLICKED':
return { activationType: 'contentsClicked' };
}

if (inArray(argsList, '-reply')) {
return { activationType: 'replied', activationValue: text };
}
if (inArray(argsList, '-action')) {
return { activationType: 'actionClicked', activationValue: text };
}
return stdout;
}
module.exports.parseMacResponse = parseMacResponse;

/**
* UNUserNotificationCenter (terminal-notifier >= 3) refuses to ask for
* notification permission for an app bundle LaunchServices has never seen,
* failing with "Notifications are not allowed for this application". A bundle
* that lives inside node_modules is exactly that, so register it once.
*/
const LSREGISTER =
'/System/Library/Frameworks/CoreServices.framework/Frameworks/' +
'LaunchServices.framework/Support/lsregister';

module.exports.isMacNotAuthorizedError = function (error, stderr) {
return (
!!error &&
error.code === MAC_EXIT_NOT_AUTHORIZED &&
/not allowed for this application/i.test(String(stderr || error.message))
);
};

module.exports.registerMacNotifier = function (notifier, cb) {
const match = /^(.*\.app)\/Contents\/MacOS\//.exec(notifier);
if (!match) return cb(new Error('Not an app bundle: ' + notifier));
cp.execFile(LSREGISTER, ['-f', match[1]], function (error) {
cb(error || null);
});
};

module.exports.immediateFileCommand = function (notifier, options, cb) {
if (process.env.DEBUG && process.env.DEBUG.indexOf('notifier') !== -1) {
console.info('node-notifier debug info (notifier):');
Expand Down Expand Up @@ -234,7 +312,12 @@ module.exports.mapToMac = function (options) {
}
}

options.json = true;
// terminal-notifier >= 3 has no way to set a custom icon or sender: the
// UserNotifications framework always uses the sending bundle's identity.
// Both flags are accepted but only produce warnings on stderr, so drop them.
delete options.appIcon;
delete options.sender;

return options;
};

Expand Down Expand Up @@ -507,13 +590,16 @@ module.exports.isMac = function () {
return os.type() === 'Darwin';
};

module.exports.isMountainLion = function () {
module.exports.isMojaveOrLater = function () {
return (
os.type() === 'Darwin' &&
semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0')
semver.satisfies(garanteeSemverFormat(os.release()), '>=18.0.0')
);
};

// Deprecated alias, kept so existing code that stubs it keeps working.
module.exports.isMountainLion = module.exports.isMojaveOrLater;

module.exports.isWin8 = function () {
return (
os.type() === 'Windows_NT' &&
Expand Down
57 changes: 51 additions & 6 deletions notifiers/notificationcenter.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const EventEmitter = require('events').EventEmitter;
const util = require('util');

const errorMessageOsX =
'You need Mac OS X 10.8 or above to use NotificationCenter,' +
'You need macOS 10.14 or above to use NotificationCenter,' +
' or use Growl fallback with constructor option {withFallback: true}.';

module.exports = NotificationCenter;
Expand Down Expand Up @@ -53,7 +53,7 @@ function notifyRaw(options, callback) {
this,
options,
callback,
function(data) {
function (data) {
if (activeId !== id) return false;

if (data === 'activate') {
Expand All @@ -76,9 +76,9 @@ function notifyRaw(options, callback) {
return this;
}

const argsList = utils.constructArgumentList(options);
if (utils.isMountainLion()) {
utils.fileCommandJson(
const argsList = constructMacArgumentList(options);
if (utils.isMojaveOrLater()) {
runNotifier(
this.options.customPath || notifier,
argsList,
actionJackedCallback
Expand All @@ -95,8 +95,53 @@ function notifyRaw(options, callback) {
return this;
}

/**
* terminal-notifier >= 3 reads `-action` and `-reply` straight from argv
* instead of through NSUserDefaults, so they must not be wrapped in quotes,
* `-action` may repeat (one per button) and `-reply` may stand alone.
*/
function constructMacArgumentList(options) {
let actions = options.actions;
const reply = options.reply;
delete options.actions;
delete options.reply;

const argsList = utils.constructArgumentList(options);

if (actions) {
actions = utils.isArray(actions) ? actions : [actions];
actions.forEach(function (action) {
argsList.push('-action', String(action));
});
}

if (typeof reply === 'string') {
argsList.push('-reply', reply);
} else if (reply) {
argsList.push('-reply');
}

return argsList;
}

function runNotifier(notifierPath, argsList, callback) {
utils.fileCommandJson(notifierPath, argsList, function (error, data) {
if (!utils.isMacNotAuthorizedError(error, data)) {
return callback(error, data);
}

// First run of the bundled binary on this machine: make LaunchServices
// aware of the app bundle so macOS can show the permission prompt, then
// try once more.
utils.registerMacNotifier(notifierPath, function (registerError) {
if (registerError) return callback(error, data);
utils.fileCommandJson(notifierPath, argsList, callback);
});
});
}

Object.defineProperty(NotificationCenter.prototype, 'notify', {
get: function() {
get: function () {
if (!this._notify) this._notify = notifyRaw.bind(this);
return this._notify;
}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
},
"keywords": [
"notification center",
"mac os x 10.8",
"mac os x 10.14",
"notify",
"terminal-notifier",
"notify-send",
Expand Down
Loading