@node-cli/timer 2.1.0 → 2.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  export * from "./defaults";
6
+ export * from "./notify";
6
7
  export * from "./parse";
7
8
  export * from "./timer";
8
9
  export * from "./utilities";
@@ -0,0 +1,27 @@
1
+ export type Notification = {
2
+ message: string;
3
+ sound?: string;
4
+ title: string;
5
+ };
6
+ export type NotifyCommand = {
7
+ args: string[];
8
+ command: string;
9
+ };
10
+ /**
11
+ * Builds the platform-native command used to display a desktop notification.
12
+ * Returns null on platforms we do not know how to notify on.
13
+ *
14
+ * Every supported platform ships its notifier as part of the OS, so nothing is
15
+ * vendored and nothing has to match the host CPU architecture.
16
+ *
17
+ */
18
+ export declare const buildNotifyCommand: (platform: string, notification: Notification) => NotifyCommand | null;
19
+ /**
20
+ * Displays a desktop notification, resolving to true when it was shown.
21
+ *
22
+ * A notification is a courtesy, never a reason to fail: an unsupported
23
+ * platform, a missing notifier, or a notifier that refuses to run all resolve
24
+ * to false instead of throwing.
25
+ *
26
+ */
27
+ export declare const notify: (notification: Notification, platform?: string) => Promise<boolean>;
package/dist/notify.js ADDED
@@ -0,0 +1,105 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify } from "node:util";
3
+ const execFileAsync = promisify(execFile);
4
+ /**
5
+ * A notifier that spawns but never exits would keep the CLI alive forever, so
6
+ * it is killed outright rather than merely asked to stop: a wedged child can
7
+ * ignore SIGTERM.
8
+ */ const NOTIFIER_TIMEOUT_MS = 5000;
9
+ /**
10
+ * Escapes a value so it can be embedded in an AppleScript double-quoted string
11
+ * literal, where only backslashes and double quotes are special.
12
+ */ const escapeAppleScript = (value)=>value.replaceAll("\\", "\\\\").replaceAll('"', '\\"');
13
+ /**
14
+ * Escapes a value so it can be embedded in a PowerShell single-quoted string
15
+ * literal, where a single quote is escaped by doubling it.
16
+ */ const escapePowerShell = (value)=>value.replaceAll("'", "''");
17
+ const macCommand = ({ message, sound, title })=>{
18
+ const script = [
19
+ `display notification "${escapeAppleScript(message)}"`,
20
+ `with title "${escapeAppleScript(title)}"`,
21
+ sound ? `sound name "${escapeAppleScript(sound)}"` : ""
22
+ ].filter(Boolean).join(" ");
23
+ return {
24
+ args: [
25
+ "-e",
26
+ script
27
+ ],
28
+ command: "osascript"
29
+ };
30
+ };
31
+ const linuxCommand = ({ message, title })=>({
32
+ args: [
33
+ title,
34
+ message
35
+ ],
36
+ command: "notify-send"
37
+ });
38
+ const windowsCommand = ({ message, title })=>{
39
+ const script = [
40
+ "[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null",
41
+ "$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)",
42
+ `$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${escapePowerShell(title)}')) > $null`,
43
+ `$xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${escapePowerShell(message)}')) > $null`,
44
+ "[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('@node-cli/timer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))"
45
+ ].join("; ");
46
+ return {
47
+ args: [
48
+ "-NoProfile",
49
+ "-Command",
50
+ script
51
+ ],
52
+ command: "powershell"
53
+ };
54
+ };
55
+ /**
56
+ * Builds the platform-native command used to display a desktop notification.
57
+ * Returns null on platforms we do not know how to notify on.
58
+ *
59
+ * Every supported platform ships its notifier as part of the OS, so nothing is
60
+ * vendored and nothing has to match the host CPU architecture.
61
+ *
62
+ */ export const buildNotifyCommand = (platform, notification)=>{
63
+ switch(platform){
64
+ case "darwin":
65
+ {
66
+ return macCommand(notification);
67
+ }
68
+ case "linux":
69
+ {
70
+ return linuxCommand(notification);
71
+ }
72
+ case "win32":
73
+ {
74
+ return windowsCommand(notification);
75
+ }
76
+ default:
77
+ {
78
+ return null;
79
+ }
80
+ }
81
+ };
82
+ /**
83
+ * Displays a desktop notification, resolving to true when it was shown.
84
+ *
85
+ * A notification is a courtesy, never a reason to fail: an unsupported
86
+ * platform, a missing notifier, or a notifier that refuses to run all resolve
87
+ * to false instead of throwing.
88
+ *
89
+ */ export const notify = async (notification, platform = process.platform)=>{
90
+ try {
91
+ const notifyCommand = buildNotifyCommand(platform, notification);
92
+ if (notifyCommand === null) {
93
+ return false;
94
+ }
95
+ await execFileAsync(notifyCommand.command, notifyCommand.args, {
96
+ killSignal: "SIGKILL",
97
+ timeout: NOTIFIER_TIMEOUT_MS
98
+ });
99
+ return true;
100
+ } catch {
101
+ return false;
102
+ }
103
+ };
104
+
105
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/notify.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execFileAsync = promisify(execFile);\n\n/**\n * A notifier that spawns but never exits would keep the CLI alive forever, so\n * it is killed outright rather than merely asked to stop: a wedged child can\n * ignore SIGTERM.\n */\nconst NOTIFIER_TIMEOUT_MS = 5000;\n\nexport type Notification = {\n\tmessage: string;\n\tsound?: string;\n\ttitle: string;\n};\n\nexport type NotifyCommand = {\n\targs: string[];\n\tcommand: string;\n};\n\n/**\n * Escapes a value so it can be embedded in an AppleScript double-quoted string\n * literal, where only backslashes and double quotes are special.\n */\nconst escapeAppleScript = (value: string): string =>\n\tvalue.replaceAll(\"\\\\\", \"\\\\\\\\\").replaceAll('\"', '\\\\\"');\n\n/**\n * Escapes a value so it can be embedded in a PowerShell single-quoted string\n * literal, where a single quote is escaped by doubling it.\n */\nconst escapePowerShell = (value: string): string => value.replaceAll(\"'\", \"''\");\n\nconst macCommand = ({ message, sound, title }: Notification): NotifyCommand => {\n\tconst script = [\n\t\t`display notification \"${escapeAppleScript(message)}\"`,\n\t\t`with title \"${escapeAppleScript(title)}\"`,\n\t\tsound ? `sound name \"${escapeAppleScript(sound)}\"` : \"\",\n\t]\n\t\t.filter(Boolean)\n\t\t.join(\" \");\n\n\treturn { args: [\"-e\", script], command: \"osascript\" };\n};\n\nconst linuxCommand = ({ message, title }: Notification): NotifyCommand => ({\n\targs: [title, message],\n\tcommand: \"notify-send\",\n});\n\nconst windowsCommand = ({ message, title }: Notification): NotifyCommand => {\n\tconst script = [\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType = WindowsRuntime] > $null\",\n\t\t\"$xml = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)\",\n\t\t`$xml.GetElementsByTagName('text')[0].AppendChild($xml.CreateTextNode('${escapePowerShell(title)}')) > $null`,\n\t\t`$xml.GetElementsByTagName('text')[1].AppendChild($xml.CreateTextNode('${escapePowerShell(message)}')) > $null`,\n\t\t\"[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier('@node-cli/timer').Show([Windows.UI.Notifications.ToastNotification]::new($xml))\",\n\t].join(\"; \");\n\n\treturn { args: [\"-NoProfile\", \"-Command\", script], command: \"powershell\" };\n};\n\n/**\n * Builds the platform-native command used to display a desktop notification.\n * Returns null on platforms we do not know how to notify on.\n *\n * Every supported platform ships its notifier as part of the OS, so nothing is\n * vendored and nothing has to match the host CPU architecture.\n *\n */\nexport const buildNotifyCommand = (\n\tplatform: string,\n\tnotification: Notification,\n): NotifyCommand | null => {\n\tswitch (platform) {\n\t\tcase \"darwin\": {\n\t\t\treturn macCommand(notification);\n\t\t}\n\t\tcase \"linux\": {\n\t\t\treturn linuxCommand(notification);\n\t\t}\n\t\tcase \"win32\": {\n\t\t\treturn windowsCommand(notification);\n\t\t}\n\t\tdefault: {\n\t\t\treturn null;\n\t\t}\n\t}\n};\n\n/**\n * Displays a desktop notification, resolving to true when it was shown.\n *\n * A notification is a courtesy, never a reason to fail: an unsupported\n * platform, a missing notifier, or a notifier that refuses to run all resolve\n * to false instead of throwing.\n *\n */\nexport const notify = async (\n\tnotification: Notification,\n\tplatform: string = process.platform,\n): Promise<boolean> => {\n\ttry {\n\t\tconst notifyCommand = buildNotifyCommand(platform, notification);\n\n\t\tif (notifyCommand === null) {\n\t\t\treturn false;\n\t\t}\n\n\t\tawait execFileAsync(notifyCommand.command, notifyCommand.args, {\n\t\t\tkillSignal: \"SIGKILL\",\n\t\t\ttimeout: NOTIFIER_TIMEOUT_MS,\n\t\t});\n\t\treturn true;\n\t} catch {\n\t\treturn false;\n\t}\n};\n"],"names":["execFile","promisify","execFileAsync","NOTIFIER_TIMEOUT_MS","escapeAppleScript","value","replaceAll","escapePowerShell","macCommand","message","sound","title","script","filter","Boolean","join","args","command","linuxCommand","windowsCommand","buildNotifyCommand","platform","notification","notify","process","notifyCommand","killSignal","timeout"],"mappings":"AAAA,SAASA,QAAQ,QAAQ,qBAAqB;AAC9C,SAASC,SAAS,QAAQ,YAAY;AAEtC,MAAMC,gBAAgBD,UAAUD;AAEhC;;;;CAIC,GACD,MAAMG,sBAAsB;AAa5B;;;CAGC,GACD,MAAMC,oBAAoB,CAACC,QAC1BA,MAAMC,UAAU,CAAC,MAAM,QAAQA,UAAU,CAAC,KAAK;AAEhD;;;CAGC,GACD,MAAMC,mBAAmB,CAACF,QAA0BA,MAAMC,UAAU,CAAC,KAAK;AAE1E,MAAME,aAAa,CAAC,EAAEC,OAAO,EAAEC,KAAK,EAAEC,KAAK,EAAgB;IAC1D,MAAMC,SAAS;QACd,CAAC,sBAAsB,EAAER,kBAAkBK,SAAS,CAAC,CAAC;QACtD,CAAC,YAAY,EAAEL,kBAAkBO,OAAO,CAAC,CAAC;QAC1CD,QAAQ,CAAC,YAAY,EAAEN,kBAAkBM,OAAO,CAAC,CAAC,GAAG;KACrD,CACCG,MAAM,CAACC,SACPC,IAAI,CAAC;IAEP,OAAO;QAAEC,MAAM;YAAC;YAAMJ;SAAO;QAAEK,SAAS;IAAY;AACrD;AAEA,MAAMC,eAAe,CAAC,EAAET,OAAO,EAAEE,KAAK,EAAgB,GAAqB,CAAA;QAC1EK,MAAM;YAACL;YAAOF;SAAQ;QACtBQ,SAAS;IACV,CAAA;AAEA,MAAME,iBAAiB,CAAC,EAAEV,OAAO,EAAEE,KAAK,EAAgB;IACvD,MAAMC,SAAS;QACd;QACA;QACA,CAAC,sEAAsE,EAAEL,iBAAiBI,OAAO,WAAW,CAAC;QAC7G,CAAC,sEAAsE,EAAEJ,iBAAiBE,SAAS,WAAW,CAAC;QAC/G;KACA,CAACM,IAAI,CAAC;IAEP,OAAO;QAAEC,MAAM;YAAC;YAAc;YAAYJ;SAAO;QAAEK,SAAS;IAAa;AAC1E;AAEA;;;;;;;CAOC,GACD,OAAO,MAAMG,qBAAqB,CACjCC,UACAC;IAEA,OAAQD;QACP,KAAK;YAAU;gBACd,OAAOb,WAAWc;YACnB;QACA,KAAK;YAAS;gBACb,OAAOJ,aAAaI;YACrB;QACA,KAAK;YAAS;gBACb,OAAOH,eAAeG;YACvB;QACA;YAAS;gBACR,OAAO;YACR;IACD;AACD,EAAE;AAEF;;;;;;;CAOC,GACD,OAAO,MAAMC,SAAS,OACrBD,cACAD,WAAmBG,QAAQH,QAAQ;IAEnC,IAAI;QACH,MAAMI,gBAAgBL,mBAAmBC,UAAUC;QAEnD,IAAIG,kBAAkB,MAAM;YAC3B,OAAO;QACR;QAEA,MAAMvB,cAAcuB,cAAcR,OAAO,EAAEQ,cAAcT,IAAI,EAAE;YAC9DU,YAAY;YACZC,SAASxB;QACV;QACA,OAAO;IACR,EAAE,OAAM;QACP,OAAO;IACR;AACD,EAAE"}
package/dist/utilities.js CHANGED
@@ -1,8 +1,19 @@
1
1
  import { Logger, Spinner } from "@node-cli/logger";
2
2
  import moment from "moment";
3
- import notifier from "node-notifier";
3
+ import { notify } from "./notify.js";
4
4
  const logger = new Logger();
5
- const spinner = new Spinner();
5
+ /**
6
+ * discardStdin must stay off. When it is on, ora puts stdin in raw mode, which
7
+ * stops the tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte
8
+ * instead. stdin-discarder means to re-send the signal itself, but it attaches
9
+ * its reader with prependListener (which, unlike on("data"), does not start a
10
+ * paused stream flowing) and only calls resume() when stdin.isPaused() was true
11
+ * beforehand. On a fresh process.stdin, flowing is null, so isPaused() is false
12
+ * and the resume never happens: the byte is never read, the signal is never
13
+ * re-sent, and Ctrl+C is swallowed for the life of the spinner.
14
+ */ const spinner = new Spinner({
15
+ discardStdin: false
16
+ });
6
17
  const UNITS = {
7
18
  ms: 1000,
8
19
  s: 1000 * 1000,
@@ -33,7 +44,7 @@ export const extractDuration = (config)=>{
33
44
  return moment.duration(totalMicroseconds / UNITS.ms);
34
45
  }
35
46
  };
36
- /* v8 ignore next 60 */ export class Timer {
47
+ /* v8 ignore start */ export class Timer {
37
48
  config;
38
49
  startTime;
39
50
  timerDurationMilliSeconds;
@@ -59,19 +70,28 @@ export const extractDuration = (config)=>{
59
70
  this.printRemainingTime();
60
71
  const timer = setInterval(this.printRemainingTime, 1000);
61
72
  // kill the spinner when the timer is done.
62
- setTimeout(()=>{
73
+ setTimeout(async ()=>{
63
74
  const message = "Time's up!";
64
- if (this.config.flags.notification) {
65
- notifier.notify({
66
- message,
67
- sound: "Funk",
68
- title: "Timer Notification",
69
- wait: true
70
- });
71
- }
72
75
  clearInterval(timer);
73
76
  this.printRemainingTime(message);
74
77
  spinner.stop();
78
+ /**
79
+ * notifying last, and never fatally: a desktop notification that cannot be
80
+ * displayed must not take the timer down with it.
81
+ */ if (this.config.flags.notification) {
82
+ try {
83
+ await notify({
84
+ message,
85
+ sound: "Funk",
86
+ title: "Timer Notification"
87
+ });
88
+ } catch {
89
+ /**
90
+ * notify is already total, so this only ever catches a future regression
91
+ * in it. Rejecting here would be fatal: an async setTimeout callback has
92
+ * nothing to attach a handler to.
93
+ */ }
94
+ }
75
95
  }, this.timerDurationMilliSeconds);
76
96
  }
77
97
  };
@@ -84,6 +104,6 @@ export const extractDuration = (config)=>{
84
104
  spinner.text = `${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;
85
105
  }
86
106
  };
87
- }
107
+ } /* v8 ignore stop */
88
108
 
89
109
  //# sourceMappingURL=utilities.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/utilities.ts"],"sourcesContent":["import { Logger, Spinner } from \"@node-cli/logger\";\nimport moment, { Duration } from \"moment\";\n\nimport notifier from \"node-notifier\";\nimport { Configuration } from \"./parse.js\";\n\nconst logger = new Logger();\nconst spinner = new Spinner();\n\nconst UNITS = {\n\tms: 1000,\n\ts: 1000 * 1000,\n\tm: 1000 * 1000 * 60,\n\th: 1000 * 1000 * 60 * 60,\n\td: 1000 * 1000 * 60 * 60 * 24,\n\tw: 1000 * 1000 * 60 * 60 * 24 * 7,\n};\n\nexport const timeToMicroseconds = (value: string, unit: string) => {\n\tconst result = UNITS[unit];\n\treturn result ? Number.parseInt(value, 10) * result : 0;\n};\n\nexport const extractDuration = (config: Configuration): Duration => {\n\tconst { parameters, showHelp } = config;\n\tlet totalMicroseconds = 0;\n\n\t/* v8 ignore next 6 */\n\tif (\n\t\tObject.entries(parameters).length === 0 ||\n\t\tObject.entries(parameters).length > 1\n\t) {\n\t\tshowHelp();\n\t}\n\n\tconst groups = parameters[\"0\"].toLowerCase().match(/[+-]?[\\d.]+[a-z]+/g);\n\n\t/* v8 ignore next 2 */\n\tif (groups === null) {\n\t\tshowHelp();\n\t} else {\n\t\tfor (const g of groups) {\n\t\t\tconst value = g.match(/[\\d.]+/g)[0];\n\t\t\tconst unit = g.match(/[a-z]+/g)[0];\n\t\t\ttotalMicroseconds += timeToMicroseconds(value, unit);\n\t\t}\n\t\treturn moment.duration(totalMicroseconds / UNITS.ms);\n\t}\n};\n\n/* v8 ignore next 60 */\nexport class Timer {\n\tconfig: Configuration;\n\tstartTime: number;\n\ttimerDurationMilliSeconds: number;\n\tintroMessage: string;\n\ttimerDurationSeconds: number;\n\n\tconstructor(config: Configuration) {\n\t\tthis.config = config;\n\t\tconst result: Duration = extractDuration(this.config);\n\t\tthis.timerDurationMilliSeconds = result.asMilliseconds();\n\t\tthis.timerDurationSeconds = result.asSeconds();\n\t\tthis.introMessage = `${result.hours()}h ${result.minutes()}m ${result.seconds()}s`;\n\t}\n\n\tstart = () => {\n\t\tif (this.timerDurationMilliSeconds > 0) {\n\t\t\tlogger.printBox(`${this.introMessage} `, {\n\t\t\t\tpadding: 0,\n\t\t\t\tborderColor: \"cyan\",\n\t\t\t\ttitle: \"Timer Configuration\",\n\t\t\t});\n\n\t\t\tthis.startTime = Date.now();\n\t\t\tspinner.start(this.introMessage);\n\n\t\t\t// update the spinner every second.\n\t\t\tthis.printRemainingTime();\n\t\t\tconst timer = setInterval(this.printRemainingTime, 1000);\n\n\t\t\t// kill the spinner when the timer is done.\n\t\t\tsetTimeout(() => {\n\t\t\t\tconst message = \"Time's up!\";\n\t\t\t\tif (this.config.flags.notification) {\n\t\t\t\t\tnotifier.notify({\n\t\t\t\t\t\tmessage,\n\t\t\t\t\t\tsound: \"Funk\",\n\t\t\t\t\t\ttitle: \"Timer Notification\",\n\t\t\t\t\t\twait: true,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tclearInterval(timer);\n\t\t\t\tthis.printRemainingTime(message);\n\t\t\t\tspinner.stop();\n\t\t\t}, this.timerDurationMilliSeconds);\n\t\t}\n\t};\n\n\tprivate printRemainingTime = (message: string | void) => {\n\t\tif (message) {\n\t\t\tspinner.text = message;\n\t\t} else {\n\t\t\tconst elapsed = (Date.now() - this.startTime) / 1000;\n\t\t\tconst duration = moment.duration(\n\t\t\t\t(this.timerDurationSeconds - elapsed) * 1000,\n\t\t\t);\n\t\t\tspinner.text = `${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;\n\t\t}\n\t};\n}\n"],"names":["Logger","Spinner","moment","notifier","logger","spinner","UNITS","ms","s","m","h","d","w","timeToMicroseconds","value","unit","result","Number","parseInt","extractDuration","config","parameters","showHelp","totalMicroseconds","Object","entries","length","groups","toLowerCase","match","g","duration","Timer","startTime","timerDurationMilliSeconds","introMessage","timerDurationSeconds","asMilliseconds","asSeconds","hours","minutes","seconds","start","printBox","padding","borderColor","title","Date","now","printRemainingTime","timer","setInterval","setTimeout","message","flags","notification","notify","sound","wait","clearInterval","stop","text","elapsed"],"mappings":"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"}
1
+ {"version":3,"sources":["../src/utilities.ts"],"sourcesContent":["import { Logger, Spinner } from \"@node-cli/logger\";\nimport moment, { Duration } from \"moment\";\n\nimport { notify } from \"./notify.js\";\nimport { Configuration } from \"./parse.js\";\n\nconst logger = new Logger();\n\n/**\n * discardStdin must stay off. When it is on, ora puts stdin in raw mode, which\n * stops the tty from turning Ctrl+C into SIGINT and delivers a bare 0x03 byte\n * instead. stdin-discarder means to re-send the signal itself, but it attaches\n * its reader with prependListener (which, unlike on(\"data\"), does not start a\n * paused stream flowing) and only calls resume() when stdin.isPaused() was true\n * beforehand. On a fresh process.stdin, flowing is null, so isPaused() is false\n * and the resume never happens: the byte is never read, the signal is never\n * re-sent, and Ctrl+C is swallowed for the life of the spinner.\n */\nconst spinner = new Spinner({ discardStdin: false });\n\nconst UNITS = {\n\tms: 1000,\n\ts: 1000 * 1000,\n\tm: 1000 * 1000 * 60,\n\th: 1000 * 1000 * 60 * 60,\n\td: 1000 * 1000 * 60 * 60 * 24,\n\tw: 1000 * 1000 * 60 * 60 * 24 * 7,\n};\n\nexport const timeToMicroseconds = (value: string, unit: string) => {\n\tconst result = UNITS[unit];\n\treturn result ? Number.parseInt(value, 10) * result : 0;\n};\n\nexport const extractDuration = (config: Configuration): Duration => {\n\tconst { parameters, showHelp } = config;\n\tlet totalMicroseconds = 0;\n\n\t/* v8 ignore next 6 */\n\tif (\n\t\tObject.entries(parameters).length === 0 ||\n\t\tObject.entries(parameters).length > 1\n\t) {\n\t\tshowHelp();\n\t}\n\n\tconst groups = parameters[\"0\"].toLowerCase().match(/[+-]?[\\d.]+[a-z]+/g);\n\n\t/* v8 ignore next 2 */\n\tif (groups === null) {\n\t\tshowHelp();\n\t} else {\n\t\tfor (const g of groups) {\n\t\t\tconst value = g.match(/[\\d.]+/g)[0];\n\t\t\tconst unit = g.match(/[a-z]+/g)[0];\n\t\t\ttotalMicroseconds += timeToMicroseconds(value, unit);\n\t\t}\n\t\treturn moment.duration(totalMicroseconds / UNITS.ms);\n\t}\n};\n\n/* v8 ignore start */\nexport class Timer {\n\tconfig: Configuration;\n\tstartTime: number;\n\ttimerDurationMilliSeconds: number;\n\tintroMessage: string;\n\ttimerDurationSeconds: number;\n\n\tconstructor(config: Configuration) {\n\t\tthis.config = config;\n\t\tconst result: Duration = extractDuration(this.config);\n\t\tthis.timerDurationMilliSeconds = result.asMilliseconds();\n\t\tthis.timerDurationSeconds = result.asSeconds();\n\t\tthis.introMessage = `${result.hours()}h ${result.minutes()}m ${result.seconds()}s`;\n\t}\n\n\tstart = () => {\n\t\tif (this.timerDurationMilliSeconds > 0) {\n\t\t\tlogger.printBox(`${this.introMessage} `, {\n\t\t\t\tpadding: 0,\n\t\t\t\tborderColor: \"cyan\",\n\t\t\t\ttitle: \"Timer Configuration\",\n\t\t\t});\n\n\t\t\tthis.startTime = Date.now();\n\t\t\tspinner.start(this.introMessage);\n\n\t\t\t// update the spinner every second.\n\t\t\tthis.printRemainingTime();\n\t\t\tconst timer = setInterval(this.printRemainingTime, 1000);\n\n\t\t\t// kill the spinner when the timer is done.\n\t\t\tsetTimeout(async () => {\n\t\t\t\tconst message = \"Time's up!\";\n\t\t\t\tclearInterval(timer);\n\t\t\t\tthis.printRemainingTime(message);\n\t\t\t\tspinner.stop();\n\n\t\t\t\t/**\n\t\t\t\t * notifying last, and never fatally: a desktop notification that cannot be\n\t\t\t\t * displayed must not take the timer down with it.\n\t\t\t\t */\n\t\t\t\tif (this.config.flags.notification) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tawait notify({\n\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\tsound: \"Funk\",\n\t\t\t\t\t\t\ttitle: \"Timer Notification\",\n\t\t\t\t\t\t});\n\t\t\t\t\t} catch {\n\t\t\t\t\t\t/**\n\t\t\t\t\t\t * notify is already total, so this only ever catches a future regression\n\t\t\t\t\t\t * in it. Rejecting here would be fatal: an async setTimeout callback has\n\t\t\t\t\t\t * nothing to attach a handler to.\n\t\t\t\t\t\t */\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}, this.timerDurationMilliSeconds);\n\t\t}\n\t};\n\n\tprivate printRemainingTime = (message: string | void) => {\n\t\tif (message) {\n\t\t\tspinner.text = message;\n\t\t} else {\n\t\t\tconst elapsed = (Date.now() - this.startTime) / 1000;\n\t\t\tconst duration = moment.duration(\n\t\t\t\t(this.timerDurationSeconds - elapsed) * 1000,\n\t\t\t);\n\t\t\tspinner.text = `${duration.hours()}h ${duration.minutes()}m ${duration.seconds()}s`;\n\t\t}\n\t};\n}\n/* v8 ignore stop */\n"],"names":["Logger","Spinner","moment","notify","logger","spinner","discardStdin","UNITS","ms","s","m","h","d","w","timeToMicroseconds","value","unit","result","Number","parseInt","extractDuration","config","parameters","showHelp","totalMicroseconds","Object","entries","length","groups","toLowerCase","match","g","duration","Timer","startTime","timerDurationMilliSeconds","introMessage","timerDurationSeconds","asMilliseconds","asSeconds","hours","minutes","seconds","start","printBox","padding","borderColor","title","Date","now","printRemainingTime","timer","setInterval","setTimeout","message","clearInterval","stop","flags","notification","sound","text","elapsed"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@node-cli/timer",
3
- "version": "2.1.0",
3
+ "version": "2.1.2",
4
4
  "license": "MIT",
5
5
  "author": "Arno Versini",
6
6
  "description": "Dead simpler CLI timer",
@@ -37,8 +37,7 @@
37
37
  "dependencies": {
38
38
  "@node-cli/logger": "2.1.0",
39
39
  "@node-cli/parser": "3.1.0",
40
- "moment": "2.30.1",
41
- "node-notifier": "10.0.1"
40
+ "moment": "2.30.1"
42
41
  },
43
42
  "publishConfig": {
44
43
  "access": "public"
@@ -47,5 +46,5 @@
47
46
  "@vitest/coverage-v8": "4.1.10",
48
47
  "vitest": "4.1.10"
49
48
  },
50
- "gitHead": "dd487a84f6d7ced2e761fb25f645fdfec7e7426b"
49
+ "gitHead": "9fdd995083ea81292321f20a3397122a18369b87"
51
50
  }