Skip to content

Issue 193 windows actions #1

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

Merged
merged 3 commits into from
Oct 28, 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);
});
});
};
97 changes: 63 additions & 34 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) {
WindowsToaster.prototype.notify = async 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,6 +121,10 @@ WindowsToaster.prototype.notify = function(options, callback) {
return fallback.notify(options, callback);
}

// Add pipeName option, to get the output
resultBuffer = await utils.createNamedPipe(namedPipe);
options.pipeName = namedPipe;

options = utils.mapToWin8(options);
var argsList = utils.constructArgumentList(options, {
explicitTrue: true,
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "node-notifier",
"version": "6.0.0",
"version": "7.0.0",
"description": "A Node.js module for sending notifications on native Mac, Windows (post and pre 8) and Linux (or Growl as fallback)",
"main": "index.js",
"scripts": {
Expand All @@ -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
6 changes: 3 additions & 3 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,16 @@ describe('constructors', function() {
expect(notifier.notify({ title: 'My notification' }, cb)).toBeTruthy();
});

it('should throw error when second parameter is not a function', function() {
it('should throw error when second parameter is not a function', async () => {
var wrongParamOne = 200;
var wrongParamTwo = 'meaningless string';
var data = { title: 'My notification' };

var base = notifier.notify.bind(notifier, data);
expect(base.bind(notifier, wrongParamOne)).toThrowError(
await expect(base.bind(notifier, wrongParamOne)()).rejects.toThrowError(
/^The second argument/
);
expect(base.bind(notifier, wrongParamTwo)).toThrowError(
await expect(base.bind(notifier, wrongParamTwo)()).rejects.toThrowError(
/^The second argument/
);
});
Expand Down
Loading