UNPKG

12.3 kBJavaScriptView Raw
1var shellwords = require('shellwords');
2var cp = require('child_process');
3var semver = require('semver');
4var path = require('path');
5var url = require('url');
6var os = require('os');
7var fs = require('fs');
8
9function clone(obj) {
10 return JSON.parse(JSON.stringify(obj));
11}
12
13module.exports.clone = clone;
14
15var escapeQuotes = function(str) {
16 if (typeof str === 'string') {
17 return str.replace(/(["$`\\])/g, '\\$1');
18 } else {
19 return str;
20 }
21};
22
23var inArray = function(arr, val) {
24 for (var i = 0; i < arr.length; i++) {
25 if (arr[i] === val) {
26 return true;
27 }
28 }
29 return false;
30};
31
32var notifySendFlags = {
33 u: 'urgency',
34 urgency: 'urgency',
35 t: 'expire-time',
36 e: 'expire-time',
37 expire: 'expire-time',
38 'expire-time': 'expire-time',
39 i: 'icon',
40 icon: 'icon',
41 c: 'category',
42 category: 'category',
43 subtitle: 'category',
44 h: 'hint',
45 hint: 'hint'
46};
47
48module.exports.command = function(notifier, options, cb) {
49 notifier = shellwords.escape(notifier);
50 if (process.env.DEBUG) {
51 console.info('node-notifier debug info (command):');
52 console.info('[notifier path]', notifier);
53 console.info('[notifier options]', options.join(' '));
54 }
55
56 return cp.exec(notifier + ' ' + options.join(' '), function(
57 error,
58 stdout,
59 stderr
60 ) {
61 if (error) return cb(error);
62 cb(stderr, stdout);
63 });
64};
65
66module.exports.fileCommand = function(notifier, options, cb) {
67 if (process.env.DEBUG) {
68 console.info('node-notifier debug info (fileCommand):');
69 console.info('[notifier path]', notifier);
70 console.info('[notifier options]', options.join(' '));
71 }
72
73 return cp.execFile(notifier, options, function(error, stdout, stderr) {
74 if (error) return cb(error, stdout);
75 cb(stderr, stdout);
76 });
77};
78
79module.exports.fileCommandJson = function(notifier, options, cb) {
80 if (process.env.DEBUG) {
81 console.info('node-notifier debug info (fileCommandJson):');
82 console.info('[notifier path]', notifier);
83 console.info('[notifier options]', options.join(' '));
84 }
85 return cp.execFile(notifier, options, function(error, stdout, stderr) {
86 if (error) return cb(error, stdout);
87 if (!stdout) return cb(error, {});
88
89 try {
90 var data = JSON.parse(stdout);
91 cb(stderr, data);
92 } catch (e) {
93 cb(e, stdout);
94 }
95 });
96};
97
98module.exports.immediateFileCommand = function(notifier, options, cb) {
99 if (process.env.DEBUG) {
100 console.info('node-notifier debug info (notifier):');
101 console.info('[notifier path]', notifier);
102 }
103
104 notifierExists(notifier, function(exists) {
105 if (!exists) {
106 return cb(new Error('Notifier (' + notifier + ') not found on system.'));
107 }
108 cp.execFile(notifier, options);
109 cb();
110 });
111};
112
113function notifierExists(notifier, cb) {
114 return fs.stat(notifier, function(err, stat) {
115 if (!err) return cb(stat.isFile());
116
117 // Check if Windows alias
118 if (path.extname(notifier)) {
119 // Has extentioon, no need to check more
120 return cb(false);
121 }
122
123 // Check if there is an exe file in the directory
124 return fs.stat(notifier + '.exe', function(err, stat) {
125 if (err) return cb(false);
126 cb(stat.isFile());
127 });
128 });
129}
130
131var mapAppIcon = function(options) {
132 if (options.appIcon) {
133 options.icon = options.appIcon;
134 delete options.appIcon;
135 }
136
137 return options;
138};
139
140var mapText = function(options) {
141 if (options.text) {
142 options.message = options.text;
143 delete options.text;
144 }
145
146 return options;
147};
148
149var mapIconShorthand = function(options) {
150 if (options.i) {
151 options.icon = options.i;
152 delete options.i;
153 }
154
155 return options;
156};
157
158module.exports.mapToNotifySend = function(options) {
159 options = mapAppIcon(options);
160 options = mapText(options);
161
162 for (var key in options) {
163 if (key === 'message' || key === 'title') continue;
164 if (options.hasOwnProperty(key) && notifySendFlags[key] !== key) {
165 options[notifySendFlags[key]] = options[key];
166 delete options[key];
167 }
168 }
169
170 return options;
171};
172
173module.exports.mapToGrowl = function(options) {
174 options = mapAppIcon(options);
175 options = mapIconShorthand(options);
176 options = mapText(options);
177
178 if (options.icon && !Buffer.isBuffer(options.icon)) {
179 try {
180 options.icon = fs.readFileSync(options.icon);
181 } catch (ex) {
182 }
183 }
184
185 return options;
186};
187
188module.exports.mapToMac = function(options) {
189 options = mapIconShorthand(options);
190 options = mapText(options);
191
192 if (options.icon) {
193 options.appIcon = options.icon;
194 delete options.icon;
195 }
196
197 if (options.sound === true) {
198 options.sound = 'Bottle';
199 }
200
201 if (options.sound === false) {
202 delete options.sound;
203 }
204
205 if (options.sound && options.sound.indexOf('Notification.') === 0) {
206 options.sound = 'Bottle';
207 }
208
209 if (options.wait === true) {
210 if (!options.timeout) {
211 options.timeout = 5;
212 }
213 delete options.wait;
214 }
215
216 options.json = true;
217 return options;
218};
219
220function isArray(arr) {
221 return Object.prototype.toString.call(arr) === '[object Array]';
222}
223
224function noop() {
225}
226module.exports.actionJackerDecorator = function(emitter, options, fn, mapper) {
227 options = clone(options);
228 fn = fn || noop;
229
230 if (typeof fn !== 'function') {
231 throw new TypeError(
232 'The second argument must be a function callback. You have passed ' +
233 typeof fn
234 );
235 }
236
237 return function(err, data) {
238 var resultantData = data;
239 var metadata = {};
240 // Allow for extra data if resultantData is an object
241 if (resultantData && typeof resultantData === 'object') {
242 metadata = resultantData;
243 resultantData = resultantData.activationType;
244 }
245
246 // Sanitize the data
247 if (resultantData) {
248 resultantData = resultantData.toLowerCase().trim();
249 if (resultantData.match(/^activate|clicked$/)) {
250 resultantData = 'activate';
251 }
252 }
253
254 fn.apply(emitter, [ err, resultantData, metadata ]);
255 if (!mapper || !resultantData) return;
256
257 var key = mapper(resultantData);
258 if (!key) return;
259 emitter.emit(key, emitter, options, metadata);
260 };
261};
262
263module.exports.constructArgumentList = function(options, extra) {
264 var args = [];
265 extra = extra || {};
266
267 // Massive ugly setup. Default args
268 var initial = extra.initial || [];
269 var keyExtra = extra.keyExtra || '';
270 var allowedArguments = extra.allowedArguments || [];
271 var noEscape = extra.noEscape !== void 0;
272 var checkForAllowed = extra.allowedArguments !== void 0;
273 var explicitTrue = !!extra.explicitTrue;
274 var keepNewlines = !!extra.keepNewlines;
275 var wrapper = extra.wrapper === void 0 ? '"' : extra.wrapper;
276
277 var escapeFn = function(arg) {
278 if (isArray(arg)) {
279 return removeNewLines(
280 arg.map(function(current) {
281 return '"' + escapeQuotes(current) + '"';
282 }).join(',')
283 );
284 }
285
286 if (!noEscape) {
287 arg = escapeQuotes(arg);
288 }
289 if (typeof arg === 'string' && !keepNewlines) {
290 arg = removeNewLines(arg);
291 }
292 return wrapper + arg + wrapper;
293 };
294
295 initial.forEach(function(val) {
296 args.push(escapeFn(val));
297 });
298 for (var key in options) {
299 if (
300 options.hasOwnProperty(key) &&
301 (!checkForAllowed || inArray(allowedArguments, key))
302 ) {
303 if (explicitTrue && options[key] === true) {
304 args.push('-' + keyExtra + key);
305 } else if (explicitTrue && options[key] === false) continue;
306 else args.push('-' + keyExtra + key, escapeFn(options[key]));
307 }
308 }
309 return args;
310};
311
312function removeNewLines(str) {
313 var excapedNewline = process.platform === 'win32' ? '\\r\\n' : '\\n';
314 return str.replace(/\r?\n/g, excapedNewline);
315}
316
317/*
318---- Options ----
319[-t] <title string> | Displayed on the first line of the toast.
320[-m] <message string> | Displayed on the remaining lines, wrapped.
321[-p] <image URI> | Display toast with an image, local files only.
322[-w] | Wait for toast to expire or activate.
323[-id] <id> | sets the id for a notification to be able to close it later.
324[-s] <sound URI> | Sets the sound of the notifications, for possible values see http://msdn.microsoft.com/en-us/library/windows/apps/hh761492.aspx.
325[-silent] | Don't play a sound file when showing the notifications.
326[-appID] <App.ID> | Don't create a shortcut but use the provided app id.
327-close <id> | Closes a currently displayed notification, in order to be able to close a notification the parameter -w must be used to create the notification.
328*/
329var allowedToasterFlags = [
330 't',
331 'm',
332 'p',
333 'w',
334 'id',
335 's',
336 'silent',
337 'appID',
338 'close',
339 'install'
340];
341var toasterSoundPrefix = 'Notification.';
342var toasterDefaultSound = 'Notification.Default';
343module.exports.mapToWin8 = function(options) {
344 options = mapAppIcon(options);
345 options = mapText(options);
346
347 if (options.icon) {
348 if (/^file:\/+/.test(options.icon)) {
349 // should parse file protocol URL to path
350 options.p = url.parse(options.icon).pathname
351 .replace(/^\/(\w:\/)/, '$1')
352 .replace(/\//g, '\\');
353 } else {
354 options.p = options.icon;
355 }
356 delete options.icon;
357 }
358
359 if (options.message) {
360 // Remove escape char to debug "HRESULT : 0xC00CE508" exception
361 options.m = options.message.replace(/\x1b/g, '');
362 delete options.message;
363 }
364
365 if (options.title) {
366 options.t = options.title;
367 delete options.title;
368 }
369
370 if (typeof options.remove !== 'undefined') {
371 options.close = options.remove;
372 delete options.remove;
373 }
374
375 if (options.quiet || options.silent) {
376 options.silent = options.quiet || options.silent;
377 delete options.quiet;
378 }
379
380 if (typeof options.sound !== 'undefined') {
381 options.s = options.sound;
382 delete options.sound;
383 }
384
385 if (options.s === false) {
386 options.silent = true;
387 delete options.s;
388 }
389
390 // Silent takes precedence. Remove sound.
391 if (options.s && options.silent) {
392 delete options.s;
393 }
394
395 if (options.s === true) {
396 options.s = toasterDefaultSound;
397 }
398
399 if (options.s && options.s.indexOf(toasterSoundPrefix) !== 0) {
400 options.s = toasterDefaultSound;
401 }
402
403 if (options.wait) {
404 options.w = options.wait;
405 delete options.wait;
406 }
407
408 for (var key in options) {
409 // Check if is allowed. If not, delete!
410 if (
411 options.hasOwnProperty(key) && allowedToasterFlags.indexOf(key) === -1
412 ) {
413 delete options[key];
414 }
415 }
416
417 return options;
418};
419
420module.exports.mapToNotifu = function(options) {
421 options = mapAppIcon(options);
422 options = mapText(options);
423
424 if (options.icon) {
425 options.i = options.icon;
426 delete options.icon;
427 }
428
429 if (options.message) {
430 options.m = options.message;
431 delete options.message;
432 }
433
434 if (options.title) {
435 options.p = options.title;
436 delete options.title;
437 }
438
439 if (options.time) {
440 options.d = options.time;
441 delete options.time;
442 }
443
444 if (options.q !== false) {
445 options.q = true;
446 } else {
447 delete options.q;
448 }
449
450 if (options.quiet === false) {
451 delete options.q;
452 delete options.quiet;
453 }
454
455 if (options.sound) {
456 delete options.q;
457 delete options.sound;
458 }
459
460 if (options.t) {
461 options.d = options.t;
462 delete options.t;
463 }
464
465 if (options.type) {
466 options.t = sanitizeNotifuTypeArgument(options.type);
467 delete options.type;
468 }
469
470 return options;
471};
472
473module.exports.isMac = function() {
474 return os.type() === 'Darwin';
475};
476
477module.exports.isMountainLion = function() {
478 return os.type() === 'Darwin' &&
479 semver.satisfies(garanteeSemverFormat(os.release()), '>=12.0.0');
480};
481
482module.exports.isWin8 = function() {
483 return os.type() === 'Windows_NT' &&
484 semver.satisfies(garanteeSemverFormat(os.release()), '>=6.2.9200');
485};
486
487module.exports.isLessThanWin8 = function() {
488 return os.type() === 'Windows_NT' &&
489 semver.satisfies(garanteeSemverFormat(os.release()), '<6.2.9200');
490};
491
492function garanteeSemverFormat(version) {
493 if (version.split('.').length === 2) {
494 version += '.0';
495 }
496 return version;
497}
498
499function sanitizeNotifuTypeArgument(type) {
500 if (typeof type === 'string' || type instanceof String) {
501 if (type.toLowerCase() === 'info') return 'info';
502 if (type.toLowerCase() === 'warn') return 'warn';
503 if (type.toLowerCase() === 'error') return 'error';
504 }
505
506 return 'info';
507}