Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue #193 - windows actions + Upgrades SnoreToast to v0.7.0 #293

Merged
merged 11 commits into from
Nov 15, 2019
Merged
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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
jnielson94 marked this conversation as resolved.
Show resolved Hide resolved
)
);

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
Original file line number Diff line number Diff line change
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
Loading