Skip to content

Commit

Permalink
Merge pull request #293 from yoavain/master
Browse files Browse the repository at this point in the history
Issue #193 - windows actions + Upgrades SnoreToast to v0.7.0
  • Loading branch information
jnielson94 committed Nov 15, 2019
2 parents 99a7984 + 0baa7a7 commit b368131
Show file tree
Hide file tree
Showing 10 changed files with 213 additions and 56 deletions.
4 changes: 4 additions & 0 deletions README.md
Expand Up @@ -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:
Expand Down
34 changes: 34 additions & 0 deletions 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');
});
10 changes: 5 additions & 5 deletions 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(
{
Expand All @@ -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!');
});
Binary file added example/windows-actions-example.gif
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
37 changes: 36 additions & 1 deletion lib/utils.js
Expand Up @@ -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));
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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]);
Expand Down Expand Up @@ -318,25 +325,30 @@ function removeNewLines(str) {
---- Options ----
[-t] <title string> | Displayed on the first line of the toast.
[-m] <message string> | Displayed on the remaining lines, wrapped.
[-b] <button1;button2 string>| Displayed on the bottom line, can list multiple buttons separated by ;
[-b] <button1;button2 string>| 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] <image URI> | Display toast with an image, local files only.
[-id] <id> | sets the id for a notification to be able to close it later.
[-s] <sound URI> | 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] <App.ID> | Don't create a shortcut but use the provided app id.
[-pid] <pid> | Query the appid for the process <pid>, 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] <C:\foo.exe> | Provide a application that might be started if the pipe does not exist.
-close <id> | Closes a currently displayed notification.
*/
var allowedToasterFlags = [
't',
'm',
'b',
'tb',
'p',
'id',
's',
'silent',
'appID',
'pid',
'pipeName',
'close',
'install'
];
Expand Down Expand Up @@ -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 (
Expand Down Expand Up @@ -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);
});
});
};
123 changes: 77 additions & 46 deletions notifiers/toaster.js
Expand Up @@ -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) {
Expand All @@ -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 };
Expand All @@ -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 (
Expand All @@ -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;
};
2 changes: 2 additions & 0 deletions package.json
Expand Up @@ -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": {
Expand Down Expand Up @@ -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": {
Expand Down

0 comments on commit b368131

Please sign in to comment.