toaster.js
  1  /**
  2   * Wrapper for the toaster (https://github.com/nels-o/toaster)
  3   */
  4  var path = require('path');
  5  var notifier = path.resolve(__dirname, '../vendor/snoreToast/snoretoast');
  6  var utils = require('../lib/utils');
  7  var Balloon = require('./balloon');
  8  var os = require('os');
  9  const { v4: uuid } = require('uuid');
 10  
 11  var EventEmitter = require('events').EventEmitter;
 12  var util = require('util');
 13  
 14  var fallback;
 15  
 16  const PIPE_NAME = 'notifierPipe';
 17  const PIPE_PATH_PREFIX = '\\\\.\\pipe\\';
 18  
 19  module.exports = WindowsToaster;
 20  
 21  function WindowsToaster(options) {
 22    options = utils.clone(options || {});
 23    if (!(this instanceof WindowsToaster)) {
 24      return new WindowsToaster(options);
 25    }
 26  
 27    this.options = options;
 28  
 29    EventEmitter.call(this);
 30  }
 31  util.inherits(WindowsToaster, EventEmitter);
 32  
 33  function noop() {}
 34  
 35  function parseResult(data) {
 36    if (!data) {
 37      return {};
 38    }
 39    return data.split(';').reduce((acc, cur) => {
 40      const split = cur.split('=');
 41      if (split && split.length === 2) {
 42        acc[split[0]] = split[1];
 43      }
 44      return acc;
 45    }, {});
 46  }
 47  
 48  function getPipeName() {
 49    return `${PIPE_PATH_PREFIX}${PIPE_NAME}-${uuid()}`;
 50  }
 51  
 52  function notifyRaw(options, callback) {
 53    options = utils.clone(options || {});
 54    callback = callback || noop;
 55    var is64Bit = os.arch() === 'x64';
 56    var resultBuffer;
 57    const server = {
 58      namedPipe: getPipeName()
 59    };
 60  
 61    if (typeof options === 'string') {
 62      options = { title: 'node-notifier', message: options };
 63    }
 64  
 65    if (typeof callback !== 'function') {
 66      throw new TypeError(
 67        'The second argument must be a function callback. You have passed ' +
 68          typeof fn
 69      );
 70    }
 71  
 72    var snoreToastResultParser = (err, callback) => {
 73      /* Possible exit statuses from SnoreToast, we only want to include err if it's -1 code
 74      Exit Status     :  Exit Code
 75      Failed          : -1
 76  
 77      Success         :  0
 78      Hidden          :  1
 79      Dismissed       :  2
 80      TimedOut        :  3
 81      ButtonPressed   :  4
 82      TextEntered     :  5
 83      */
 84      const result = parseResult(
 85        resultBuffer && resultBuffer.toString('utf16le')
 86      );
 87  
 88      // parse action
 89      if (result.action === 'buttonClicked' && result.button) {
 90        result.activationType = result.button;
 91      } else if (result.action) {
 92        result.activationType = result.action;
 93      }
 94  
 95      if (err && err.code === -1) {
 96        callback(err, result);
 97      }
 98      callback(null, result);
 99  
100      // https://github.com/mikaelbr/node-notifier/issues/334
101      // Due to an issue with snoretoast not using stdio and pipe
102      // when notifications are disabled, make sure named pipe server
103      // is closed before exiting.
104      server.instance && server.instance.close();
105    };
106  
107    var actionJackedCallback = (err) =>
108      snoreToastResultParser(
109        err,
110        utils.actionJackerDecorator(
111          this,
112          options,
113          callback,
114          (data) => data || false
115        )
116      );
117  
118    options.title = options.title || 'Node Notification:';
119    if (
120      typeof options.message === 'undefined' &&
121      typeof options.close === 'undefined'
122    ) {
123      callback(new Error('Message or ID to close is required.'));
124      return this;
125    }
126  
127    if (!utils.isWin8() && !utils.isWSL() && !!this.options.withFallback) {
128      fallback = fallback || new Balloon(this.options);
129      return fallback.notify(options, callback);
130    }
131  
132    // Add pipeName option, to get the output
133    utils.createNamedPipe(server).then((out) => {
134      resultBuffer = out;
135      options.pipeName = server.namedPipe;
136  
137      options = utils.mapToWin8(options);
138      var argsList = utils.constructArgumentList(options, {
139        explicitTrue: true,
140        wrapper: '',
141        keepNewlines: true,
142        noEscape: true
143      });
144  
145      var notifierWithArch = notifier + '-x' + (is64Bit ? '64' : '86') + '.exe';
146      utils.fileCommand(
147        this.options.customPath || notifierWithArch,
148        argsList,
149        actionJackedCallback
150      );
151    });
152    return this;
153  }
154  
155  Object.defineProperty(WindowsToaster.prototype, 'notify', {
156    get: function () {
157      if (!this._notify) this._notify = notifyRaw.bind(this);
158      return this._notify;
159    }
160  });