diff --git a/README.md b/README.md
index 6c725fd..8e61a16 100644
--- a/README.md
+++ b/README.md
@@ -12,6 +12,10 @@ earlier Windows versions. Growl is used if none of these requirements are met.
![Input Example](https://raw.githubusercontent.com/mikaelbr/node-notifier/master/example/input-example.gif)
+## Actions Example Windows SnoreToast
+
+![Actions Example](https://raw.githubusercontent.com/mikaelbr/node-notifier/master/example/windows-actions-example.gif)
+
## Quick Usage
Show a native notification on macOS, Windows, Linux:
diff --git a/example/toaster-with-actions.js b/example/toaster-with-actions.js
new file mode 100644
index 0000000..597ebda
--- /dev/null
+++ b/example/toaster-with-actions.js
@@ -0,0 +1,34 @@
+const notifier = require('../index');
+const path = require('path');
+
+notifier.notify(
+ {
+ message: 'Are you sure you want to continue?',
+ icon: path.join(__dirname, 'coulson.jpg'),
+ actions: ['OK', 'Cancel']
+ },
+ (err, data) => {
+ // Will also wait until notification is closed.
+ console.log('Waited');
+ console.log(JSON.stringify({ err, data }, null, '\t'));
+ }
+);
+
+// Built-in actions:
+notifier.on('timeout', () => {
+ console.log('Timed out!');
+});
+notifier.on('activate', () => {
+ console.log('Clicked!');
+});
+notifier.on('dismissed', () => {
+ console.log('Dismissed!');
+});
+
+// Buttons actions (lower-case):
+notifier.on('ok', () => {
+ console.log('"Ok" was pressed');
+});
+notifier.on('cancel', () => {
+ console.log('"Cancel" was pressed');
+});
diff --git a/example/toaster.js b/example/toaster.js
index fd04f80..8e5df79 100644
--- a/example/toaster.js
+++ b/example/toaster.js
@@ -1,5 +1,5 @@
-var notifier = require('../index');
-var path = require('path');
+const notifier = require('../index');
+const path = require('path');
notifier.notify(
{
@@ -10,14 +10,14 @@ notifier.notify(
function(err, data) {
// Will also wait until notification is closed.
console.log('Waited');
- console.log(err, data);
+ console.log(JSON.stringify({ err, data }));
}
);
-notifier.on('timeout', function() {
+notifier.on('timeout', () => {
console.log('Timed out!');
});
-notifier.on('click', function() {
+notifier.on('click', () => {
console.log('Clicked!');
});
diff --git a/example/windows-actions-example.gif b/example/windows-actions-example.gif
new file mode 100644
index 0000000..dc03aef
Binary files /dev/null and b/example/windows-actions-example.gif differ
diff --git a/lib/utils.js b/lib/utils.js
index f6e803c..1870cf8 100644
--- a/lib/utils.js
+++ b/lib/utils.js
@@ -6,6 +6,9 @@ var path = require('path');
var url = require('url');
var os = require('os');
var fs = require('fs');
+var net = require('net');
+
+const BUFFER_SIZE = 1024;
function clone(obj) {
return JSON.parse(JSON.stringify(obj));
@@ -225,6 +228,7 @@ module.exports.mapToMac = function(options) {
function isArray(arr) {
return Object.prototype.toString.call(arr) === '[object Array]';
}
+module.exports.isArray = isArray;
function noop() {}
module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
@@ -253,6 +257,9 @@ module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
if (resultantData.match(/^activate|clicked$/)) {
resultantData = 'activate';
}
+ if (resultantData.match(/^timedout$/)) {
+ resultantData = 'timeout';
+ }
}
fn.apply(emitter, [err, resultantData, metadata]);
@@ -318,13 +325,14 @@ function removeNewLines(str) {
---- Options ----
[-t]
| Displayed on the first line of the toast.
[-m] | Displayed on the remaining lines, wrapped.
-[-b] | Displayed on the bottom line, can list multiple buttons separated by ;
+[-b] | Displayed on the bottom line, can list multiple buttons separated by ";"
[-tb] | Displayed a textbox on the bottom line, only if buttons are not presented.
[-p] | Display toast with an image, local files only.
[-id] | sets the id for a notification to be able to close it later.
[-s] | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
[-silent] | Don't play a sound file when showing the notifications.
[-appID] | Don't create a shortcut but use the provided app id.
+[-pid] | Query the appid for the process , use -appID as fallback. (Only relevant for applications that might be packaged for the store)
[-pipeName] <\.\pipe\pipeName\> | Provide a name pipe which is used for callbacks.
[-application] | Provide a application that might be started if the pipe does not exist.
-close | Closes a currently displayed notification.
@@ -332,11 +340,15 @@ function removeNewLines(str) {
var allowedToasterFlags = [
't',
'm',
+ 'b',
+ 'tb',
'p',
'id',
's',
'silent',
'appID',
+ 'pid',
+ 'pipeName',
'close',
'install'
];
@@ -407,6 +419,11 @@ module.exports.mapToWin8 = function(options) {
options.s = toasterDefaultSound;
}
+ if (options.actions && isArray(options.actions)) {
+ options.b = options.actions.join(';');
+ delete options.actions;
+ }
+
for (var key in options) {
// Check if is allowed. If not, delete!
if (
@@ -518,3 +535,21 @@ function sanitizeNotifuTypeArgument(type) {
return 'info';
}
+
+module.exports.createNamedPipe = namedPipe => {
+ const buf = Buffer.alloc(BUFFER_SIZE);
+
+ return new Promise(resolve => {
+ const server = net.createServer(stream => {
+ stream.on('data', c => {
+ buf.write(c.toString());
+ });
+ stream.on('end', () => {
+ server.close();
+ });
+ });
+ server.listen(namedPipe, () => {
+ resolve(buf);
+ });
+ });
+};
diff --git a/notifiers/toaster.js b/notifiers/toaster.js
index 682c47e..4269d0b 100644
--- a/notifiers/toaster.js
+++ b/notifiers/toaster.js
@@ -6,12 +6,16 @@ var notifier = path.resolve(__dirname, '../vendor/snoreToast/snoretoast');
var utils = require('../lib/utils');
var Balloon = require('./balloon');
var os = require('os');
+const uuid = require('uuid/v4');
var EventEmitter = require('events').EventEmitter;
var util = require('util');
var fallback;
+const PIPE_NAME = 'notifierPipe';
+const PIPE_PATH_PREFIX = '\\\\.\\pipe\\';
+
module.exports = WindowsToaster;
function WindowsToaster(options) {
@@ -28,17 +32,29 @@ util.inherits(WindowsToaster, EventEmitter);
function noop() {}
-var timeoutMessage = 'the toast has timed out';
-var successMessage = 'user clicked on the toast';
+function parseResult(data) {
+ if (!data) {
+ return {};
+ }
+ return data.split(';').reduce((acc, cur) => {
+ const split = cur.split('=');
+ if (split && split.length === 2) {
+ acc[split[0]] = split[1];
+ }
+ return acc;
+ }, {});
+}
-function hasText(str, txt) {
- return str && str.indexOf(txt) !== -1;
+function getPipeName() {
+ return `${PIPE_PATH_PREFIX}${PIPE_NAME}-${uuid()}`;
}
WindowsToaster.prototype.notify = function(options, callback) {
options = utils.clone(options || {});
callback = callback || noop;
var is64Bit = os.arch() === 'x64';
+ var resultBuffer;
+ const namedPipe = getPipeName();
if (typeof options === 'string') {
options = { title: 'node-notifier', message: options };
@@ -51,36 +67,45 @@ WindowsToaster.prototype.notify = function(options, callback) {
);
}
- var actionJackedCallback = utils.actionJackerDecorator(
- this,
- options,
- function cb(err, data) {
- /* Possible exit statuses from SnoreToast, we only want to include err if it's -1 code
- Exit Status : Exit Code
- Failed : -1
-
- Success : 0
- Hidden : 1
- Dismissed : 2
- TimedOut : 3
- ButtonPressed : 4
- TextEntered : 5
- */
- if (err && err.code !== -1) {
- return callback(null, data);
- }
- callback(err, data);
- },
- function mapper(data) {
- if (hasText(data, successMessage)) {
- return 'click';
- }
- if (hasText(data, timeoutMessage)) {
- return 'timeout';
- }
- return false;
+ var snoreToastResultParser = (err, callback) => {
+ /* Possible exit statuses from SnoreToast, we only want to include err if it's -1 code
+ Exit Status : Exit Code
+ Failed : -1
+
+ Success : 0
+ Hidden : 1
+ Dismissed : 2
+ TimedOut : 3
+ ButtonPressed : 4
+ TextEntered : 5
+ */
+ const result = parseResult(
+ resultBuffer && resultBuffer.toString('utf16le')
+ );
+
+ // parse action
+ if (result.action === 'buttonClicked' && result.button) {
+ result.activationType = result.button;
+ } else if (result.action) {
+ result.activationType = result.action;
}
- );
+
+ if (err && err.code === -1) {
+ callback(err, result);
+ }
+ callback(null, result);
+ };
+
+ var actionJackedCallback = err =>
+ snoreToastResultParser(
+ err,
+ utils.actionJackerDecorator(
+ this,
+ options,
+ callback,
+ data => data || false
+ )
+ );
options.title = options.title || 'Node Notification:';
if (
@@ -96,19 +121,25 @@ WindowsToaster.prototype.notify = function(options, callback) {
return fallback.notify(options, callback);
}
- options = utils.mapToWin8(options);
- var argsList = utils.constructArgumentList(options, {
- explicitTrue: true,
- wrapper: '',
- keepNewlines: true,
- noEscape: true
+ // Add pipeName option, to get the output
+ utils.createNamedPipe(namedPipe).then(out => {
+ resultBuffer = out;
+ options.pipeName = namedPipe;
+
+ options = utils.mapToWin8(options);
+ var argsList = utils.constructArgumentList(options, {
+ explicitTrue: true,
+ wrapper: '',
+ keepNewlines: true,
+ noEscape: true
+ });
+
+ var notifierWithArch = notifier + '-x' + (is64Bit ? '64' : '86') + '.exe';
+ utils.fileCommand(
+ this.options.customPath || notifierWithArch,
+ argsList,
+ actionJackedCallback
+ );
});
-
- var notifierWithArch = notifier + '-x' + (is64Bit ? '64' : '86') + '.exe';
- utils.fileCommand(
- this.options.customPath || notifierWithArch,
- argsList,
- actionJackedCallback
- );
return this;
};
diff --git a/package.json b/package.json
index 3a793a0..e132219 100644
--- a/package.json
+++ b/package.json
@@ -10,6 +10,7 @@
"example:mac": "node ./example/advanced.js",
"example:mac:input": "node ./example/macInput.js",
"example:windows": "node ./example/toaster.js",
+ "example:windows:actions": "node ./example/toaster-with-actions.js",
"lint": "eslint example/*.js lib/*.js notifiers/*.js test/**/*.js index.js"
},
"jest": {
@@ -54,6 +55,7 @@
"is-wsl": "^2.1.1",
"semver": "^6.3.0",
"shellwords": "^0.1.1",
+ "uuid": "^3.3.3",
"which": "^1.3.1"
},
"husky": {
diff --git a/test/toaster.js b/test/toaster.js
index 396cba3..41024f8 100644
--- a/test/toaster.js
+++ b/test/toaster.js
@@ -3,9 +3,13 @@ var utils = require('../lib/utils');
var path = require('path');
var os = require('os');
var testUtils = require('./_test-utils');
+jest.mock('uuid/v4', () => {
+ return () => '123456789';
+});
describe('WindowsToaster', function() {
var original = utils.fileCommand;
+ var createNamedPipe = utils.createNamedPipe;
var originalType = os.type;
var originalArch = os.arch;
var originalRelease = os.release;
@@ -17,10 +21,12 @@ describe('WindowsToaster', function() {
os.type = function() {
return 'Windows_NT';
};
+ utils.createNamedPipe = () => Promise.resolve(Buffer.from('12345'));
});
afterEach(function() {
utils.fileCommand = original;
+ utils.createNamedPipe = createNamedPipe;
os.type = originalType;
os.arch = originalArch;
os.release = originalRelease;
@@ -30,9 +36,11 @@ describe('WindowsToaster', function() {
utils.fileCommand = function(notifier, argsList, callback) {
expect(testUtils.argsListHas(argsList, '-t')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-m')).toBeTruthy();
+ expect(testUtils.argsListHas(argsList, '-b')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-p')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-id')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-appID')).toBeTruthy();
+ expect(testUtils.argsListHas(argsList, '-pipeName')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-install')).toBeTruthy();
expect(testUtils.argsListHas(argsList, '-close')).toBeTruthy();
@@ -40,6 +48,8 @@ describe('WindowsToaster', function() {
expect(testUtils.argsListHas(argsList, '-bar')).toBeFalsy();
expect(testUtils.argsListHas(argsList, '-message')).toBeFalsy();
expect(testUtils.argsListHas(argsList, '-title')).toBeFalsy();
+ expect(testUtils.argsListHas(argsList, '-tb')).toBeFalsy();
+ expect(testUtils.argsListHas(argsList, '-pid')).toBeFalsy();
done();
};
var notifier = new Notify();
@@ -55,7 +65,8 @@ describe('WindowsToaster', function() {
appID: 123,
icon: 'file:///C:/node-notifier/test/fixture/coulson.jpg',
id: 1337,
- sound: 'Notification.IM'
+ sound: 'Notification.IM',
+ actions: ['Ok', 'Cancel']
});
});
@@ -244,7 +255,7 @@ describe('WindowsToaster', function() {
it('should parse file protocol URL of icon', function(done) {
utils.fileCommand = function(notifier, argsList, callback) {
- expect(argsList[1]).toBe('C:\\node-notifier\\test\\fixture\\coulson.jpg');
+ expect(argsList[3]).toBe('C:\\node-notifier\\test\\fixture\\coulson.jpg');
done();
};
@@ -260,7 +271,7 @@ describe('WindowsToaster', function() {
it('should not parse local path of icon', function(done) {
var icon = path.join(__dirname, 'fixture', 'coulson.jpg');
utils.fileCommand = function(notifier, argsList, callback) {
- expect(argsList[1]).toBe(icon);
+ expect(argsList[3]).toBe(icon);
done();
};
@@ -271,11 +282,51 @@ describe('WindowsToaster', function() {
it('should not parse normal URL of icon', function(done) {
var icon = 'http://csscomb.com/img/csscomb.jpg';
utils.fileCommand = function(notifier, argsList, callback) {
- expect(argsList[1]).toBe(icon);
+ expect(argsList[3]).toBe(icon);
done();
};
var notifier = new Notify();
notifier.notify({ title: 'Heya', message: 'foo bar', icon: icon });
});
+
+ it('should build command-line argument for actions array properly', () => {
+ utils.fileCommand = function(notifier, argsList, callback) {
+ expect(argsList).toEqual([
+ '-close',
+ '123',
+ '-install',
+ '/dsa/',
+ '-id',
+ '1337',
+ '-pipeName',
+ '\\\\.\\pipe\\notifierPipe-123456789',
+ '-p',
+ 'C:\\node-notifier\\test\\fixture\\coulson.jpg',
+ '-m',
+ 'foo bar',
+ '-t',
+ 'Heya',
+ '-s',
+ 'Notification.IM',
+ '-b',
+ 'Ok;Cancel'
+ ]);
+ };
+ var notifier = new Notify();
+
+ notifier.notify({
+ title: 'Heya',
+ message: 'foo bar',
+ extra: 'dsakdsa',
+ foo: 'bar',
+ close: 123,
+ bar: true,
+ install: '/dsa/',
+ icon: 'file:///C:/node-notifier/test/fixture/coulson.jpg',
+ id: 1337,
+ sound: 'Notification.IM',
+ actions: ['Ok', 'Cancel']
+ });
+ });
});
diff --git a/vendor/snoreToast/snoretoast-x64.exe b/vendor/snoreToast/snoretoast-x64.exe
index 1a71c72..44b1422 100644
Binary files a/vendor/snoreToast/snoretoast-x64.exe and b/vendor/snoreToast/snoretoast-x64.exe differ
diff --git a/vendor/snoreToast/snoretoast-x86.exe b/vendor/snoreToast/snoretoast-x86.exe
index 55ddb2a..908afb9 100644
Binary files a/vendor/snoreToast/snoretoast-x86.exe and b/vendor/snoreToast/snoretoast-x86.exe differ