pomotty 1.0.1 → 1.2.0
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/LICENSE +21 -0
- package/README.ja.md +54 -4
- package/README.md +56 -4
- package/dist/cli.js +1621 -0
- package/package.json +16 -11
- package/dist/bin/pomotty.js +0 -483
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import process$1 from "node:process";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { accessSync, constants, realpathSync, statSync, writeSync } from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
+
import { on } from "node:events";
|
|
9
|
+
import { emitKeypressEvents } from "node:readline";
|
|
10
|
+
//#region src/diagnostics/writer.ts
|
|
11
|
+
/**
|
|
12
|
+
* 診断の出力先が閉じていても失敗しない書き込み処理を生成します。
|
|
13
|
+
*
|
|
14
|
+
* 最初の書き込みより前に`error`リスナーを登録し、閉じたパイプなどで
|
|
15
|
+
* 発生する非同期エラーを未処理の例外にしません。同期例外も握りつぶし、
|
|
16
|
+
* 一度でも書き込みに失敗したら以後の診断を出力しません。
|
|
17
|
+
* これにより出力先の状態が終了コードへ影響しなくなります。
|
|
18
|
+
*
|
|
19
|
+
* @param stream 診断の出力先。
|
|
20
|
+
* @returns 出力先の状態にかかわらず例外を投げない書き込み処理。
|
|
21
|
+
*/
|
|
22
|
+
const createDiagnosticWriter = (stream = process$1.stderr) => {
|
|
23
|
+
let unavailable = false;
|
|
24
|
+
stream.on("error", () => {
|
|
25
|
+
unavailable = true;
|
|
26
|
+
});
|
|
27
|
+
return (output) => {
|
|
28
|
+
if (unavailable) return;
|
|
29
|
+
try {
|
|
30
|
+
stream.write(output);
|
|
31
|
+
} catch {
|
|
32
|
+
unavailable = true;
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
//#endregion
|
|
37
|
+
//#region src/notification/operation.ts
|
|
38
|
+
const SUCCESS_EXIT_CODE$1 = 0;
|
|
39
|
+
const EMPTY_SIZE = 0;
|
|
40
|
+
const PLAYER_TIMEOUT_MS = 1e4;
|
|
41
|
+
const FORCE_KILL_DELAY_MS = 1e3;
|
|
42
|
+
const COMPLETION_GRACE_MS = 3e3;
|
|
43
|
+
const DETACH_DELAY_MS = 1500;
|
|
44
|
+
const bestEffort = (action) => {
|
|
45
|
+
try {
|
|
46
|
+
action();
|
|
47
|
+
} catch {}
|
|
48
|
+
};
|
|
49
|
+
const setUnrefTimeout = (callback, delayMs) => {
|
|
50
|
+
const timer = setTimeout(callback, delayMs);
|
|
51
|
+
timer.unref();
|
|
52
|
+
return timer;
|
|
53
|
+
};
|
|
54
|
+
const clearTimers = (timers) => {
|
|
55
|
+
for (const timer of timers) clearTimeout(timer);
|
|
56
|
+
timers.clear();
|
|
57
|
+
};
|
|
58
|
+
const spawnNodeChild = (commandPath, commandArguments, { env }) => {
|
|
59
|
+
const child = spawn(commandPath, [...commandArguments], {
|
|
60
|
+
env,
|
|
61
|
+
shell: false,
|
|
62
|
+
stdio: "ignore",
|
|
63
|
+
windowsHide: true
|
|
64
|
+
});
|
|
65
|
+
return {
|
|
66
|
+
detach: () => {
|
|
67
|
+
child.stdin?.destroy();
|
|
68
|
+
child.stdout?.destroy();
|
|
69
|
+
child.stderr?.destroy();
|
|
70
|
+
child.unref();
|
|
71
|
+
},
|
|
72
|
+
kill: (signal) => {
|
|
73
|
+
child.kill(signal);
|
|
74
|
+
},
|
|
75
|
+
onClose: (listener) => {
|
|
76
|
+
child.once("close", listener);
|
|
77
|
+
},
|
|
78
|
+
onError: (listener) => {
|
|
79
|
+
child.on("error", listener);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
const createOperation = (completionNotice) => {
|
|
84
|
+
const callbacks = /* @__PURE__ */ new Set();
|
|
85
|
+
let cancelled = false;
|
|
86
|
+
return {
|
|
87
|
+
cancel: () => {
|
|
88
|
+
if (cancelled) return;
|
|
89
|
+
cancelled = true;
|
|
90
|
+
for (const callback of callbacks) bestEffort(callback);
|
|
91
|
+
callbacks.clear();
|
|
92
|
+
},
|
|
93
|
+
completionNotice,
|
|
94
|
+
isCancelled: () => cancelled,
|
|
95
|
+
onCancel: (callback) => {
|
|
96
|
+
if (cancelled) {
|
|
97
|
+
callback();
|
|
98
|
+
return () => {};
|
|
99
|
+
}
|
|
100
|
+
callbacks.add(callback);
|
|
101
|
+
return () => {
|
|
102
|
+
callbacks.delete(callback);
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
};
|
|
107
|
+
const createOperationTracker = (spawnChild = spawnNodeChild) => {
|
|
108
|
+
const activeChildren = /* @__PURE__ */ new Set(), activeOperations = /* @__PURE__ */ new Map(), forceKillTimers = /* @__PURE__ */ new Set(), terminationTimers = /* @__PURE__ */ new Set();
|
|
109
|
+
let shuttingDown = false, shutdownPromise;
|
|
110
|
+
const isOperationCancelled = (operation) => operation.isCancelled() || shuttingDown && !operation.completionNotice, scheduleForceKill = (child) => {
|
|
111
|
+
const timer = setUnrefTimeout(() => {
|
|
112
|
+
forceKillTimers.delete(timer);
|
|
113
|
+
if (activeChildren.has(child)) bestEffort(() => {
|
|
114
|
+
child.kill("SIGKILL");
|
|
115
|
+
});
|
|
116
|
+
}, FORCE_KILL_DELAY_MS);
|
|
117
|
+
forceKillTimers.add(timer);
|
|
118
|
+
}, spawnTracked = (operation, commandPath, commandArguments, options = {}) => {
|
|
119
|
+
if (isOperationCancelled(operation)) return { result: "cancelled" };
|
|
120
|
+
let child;
|
|
121
|
+
try {
|
|
122
|
+
child = spawnChild(commandPath, commandArguments, { env: options.env });
|
|
123
|
+
} catch {
|
|
124
|
+
return { result: "failure" };
|
|
125
|
+
}
|
|
126
|
+
activeChildren.add(child);
|
|
127
|
+
child.onError(() => {});
|
|
128
|
+
child.onClose(() => {
|
|
129
|
+
activeChildren.delete(child);
|
|
130
|
+
if (activeChildren.size === EMPTY_SIZE) clearTimers(terminationTimers);
|
|
131
|
+
});
|
|
132
|
+
if (isOperationCancelled(operation)) {
|
|
133
|
+
bestEffort(() => {
|
|
134
|
+
child.kill();
|
|
135
|
+
});
|
|
136
|
+
return { result: "cancelled" };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
child,
|
|
140
|
+
result: "success"
|
|
141
|
+
};
|
|
142
|
+
}, runChild = (operation, commandPath, commandArguments, { env, timeoutMs = PLAYER_TIMEOUT_MS } = {}) => new Promise((resolve) => {
|
|
143
|
+
if (isOperationCancelled(operation)) {
|
|
144
|
+
resolve("cancelled");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const cleanups = [];
|
|
148
|
+
let settled = false;
|
|
149
|
+
const finish = (result) => {
|
|
150
|
+
if (settled) return;
|
|
151
|
+
settled = true;
|
|
152
|
+
for (const cleanup of cleanups) cleanup();
|
|
153
|
+
if (isOperationCancelled(operation)) resolve("cancelled");
|
|
154
|
+
else resolve(result);
|
|
155
|
+
};
|
|
156
|
+
cleanups.push(operation.onCancel(() => {
|
|
157
|
+
finish("cancelled");
|
|
158
|
+
}));
|
|
159
|
+
const spawned = spawnTracked(operation, commandPath, commandArguments, { env });
|
|
160
|
+
if (spawned.result !== "success") {
|
|
161
|
+
finish(spawned.result);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
const { child } = spawned;
|
|
165
|
+
child.onError(() => {
|
|
166
|
+
finish("failure");
|
|
167
|
+
});
|
|
168
|
+
child.onClose((code, signal) => {
|
|
169
|
+
if (code === SUCCESS_EXIT_CODE$1 && signal === null) finish("success");
|
|
170
|
+
else finish("failure");
|
|
171
|
+
});
|
|
172
|
+
const timeout = setTimeout(() => {
|
|
173
|
+
bestEffort(() => {
|
|
174
|
+
child.kill();
|
|
175
|
+
});
|
|
176
|
+
scheduleForceKill(child);
|
|
177
|
+
finish("failure");
|
|
178
|
+
}, timeoutMs);
|
|
179
|
+
cleanups.push(() => {
|
|
180
|
+
clearTimeout(timeout);
|
|
181
|
+
});
|
|
182
|
+
}), terminateChildren = () => {
|
|
183
|
+
clearTimers(forceKillTimers);
|
|
184
|
+
if (activeChildren.size === EMPTY_SIZE) return;
|
|
185
|
+
for (const child of activeChildren) bestEffort(() => {
|
|
186
|
+
child.kill();
|
|
187
|
+
});
|
|
188
|
+
terminationTimers.add(setUnrefTimeout(() => {
|
|
189
|
+
for (const child of activeChildren) bestEffort(() => {
|
|
190
|
+
child.kill("SIGKILL");
|
|
191
|
+
});
|
|
192
|
+
}, 500));
|
|
193
|
+
terminationTimers.add(setUnrefTimeout(() => {
|
|
194
|
+
for (const child of activeChildren) bestEffort(() => {
|
|
195
|
+
child.detach();
|
|
196
|
+
});
|
|
197
|
+
}, DETACH_DELAY_MS));
|
|
198
|
+
}, waitForCompletionNotices = () => {
|
|
199
|
+
const pending = [...activeOperations].filter(([operation]) => operation.completionNotice).map(([, promise]) => promise);
|
|
200
|
+
if (pending.length === EMPTY_SIZE) return Promise.resolve();
|
|
201
|
+
return new Promise((resolve) => {
|
|
202
|
+
const timer = setUnrefTimeout(resolve, COMPLETION_GRACE_MS);
|
|
203
|
+
Promise.all(pending).then(() => {
|
|
204
|
+
clearTimeout(timer);
|
|
205
|
+
resolve();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}, cancelOperations = (filter) => {
|
|
209
|
+
for (const operation of activeOperations.keys()) if (filter(operation)) operation.cancel();
|
|
210
|
+
};
|
|
211
|
+
return {
|
|
212
|
+
activeChildCount: () => activeChildren.size,
|
|
213
|
+
isOperationCancelled,
|
|
214
|
+
runChild,
|
|
215
|
+
shutdown: ({ complete }) => {
|
|
216
|
+
if (shutdownPromise) return shutdownPromise;
|
|
217
|
+
shuttingDown = true;
|
|
218
|
+
cancelOperations((operation) => !complete || !operation.completionNotice);
|
|
219
|
+
let grace = Promise.resolve();
|
|
220
|
+
if (complete) grace = waitForCompletionNotices();
|
|
221
|
+
shutdownPromise = grace.then(() => {
|
|
222
|
+
cancelOperations(() => true);
|
|
223
|
+
terminateChildren();
|
|
224
|
+
});
|
|
225
|
+
return shutdownPromise;
|
|
226
|
+
},
|
|
227
|
+
spawnTracked,
|
|
228
|
+
startOperation: (completionNotice, run) => {
|
|
229
|
+
if (shuttingDown) return;
|
|
230
|
+
const operation = createOperation(completionNotice), promise = run(operation).then(() => {}).catch(() => {}).finally(() => {
|
|
231
|
+
activeOperations.delete(operation);
|
|
232
|
+
});
|
|
233
|
+
activeOperations.set(operation, promise);
|
|
234
|
+
return operation;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
//#endregion
|
|
239
|
+
//#region src/notification/sound.ts
|
|
240
|
+
const FALLBACK_SOUNDS = {
|
|
241
|
+
break: "\x07\x07",
|
|
242
|
+
work: "\x07"
|
|
243
|
+
};
|
|
244
|
+
const WINDOWS_SOUND_SCRIPT = [
|
|
245
|
+
"$player = New-Object System.Media.SoundPlayer",
|
|
246
|
+
"$player.SoundLocation = $env:POMOTTY_SOUND_FILE",
|
|
247
|
+
"$player.PlaySync()"
|
|
248
|
+
].join("; ");
|
|
249
|
+
const soundFileFor = (soundDirectory, phase) => fileURLToPath(new URL(`${phase}-end.wav`, soundDirectory));
|
|
250
|
+
const commandsFor = (commands, soundFile, environment) => {
|
|
251
|
+
const candidates = [];
|
|
252
|
+
for (const commandPath of [
|
|
253
|
+
commands.afplay,
|
|
254
|
+
commands.paplay,
|
|
255
|
+
commands.aplay
|
|
256
|
+
]) if (commandPath !== void 0) candidates.push({
|
|
257
|
+
commandArguments: [soundFile],
|
|
258
|
+
commandPath
|
|
259
|
+
});
|
|
260
|
+
if (commands.powershell !== void 0) candidates.push({
|
|
261
|
+
commandArguments: [
|
|
262
|
+
"-NoProfile",
|
|
263
|
+
"-NonInteractive",
|
|
264
|
+
"-Command",
|
|
265
|
+
WINDOWS_SOUND_SCRIPT
|
|
266
|
+
],
|
|
267
|
+
commandPath: commands.powershell,
|
|
268
|
+
env: {
|
|
269
|
+
...environment,
|
|
270
|
+
POMOTTY_SOUND_FILE: soundFile
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return candidates;
|
|
274
|
+
};
|
|
275
|
+
const runSoundCommands = async (operation, tracker, candidates, fallback) => {
|
|
276
|
+
for (const candidate of candidates) if (await tracker.runChild(operation, candidate.commandPath, candidate.commandArguments, { env: candidate.env }) !== "failure" || tracker.isOperationCancelled(operation)) return;
|
|
277
|
+
if (!tracker.isOperationCancelled(operation)) fallback();
|
|
278
|
+
};
|
|
279
|
+
const createSoundPlayer = ({ commands, environment = process$1.env, soundDirectory, spawnChild, tracker = createOperationTracker(spawnChild), writeFallback = createDiagnosticWriter() }) => ({
|
|
280
|
+
play: (phase, { completionNotice = false } = {}) => {
|
|
281
|
+
const candidates = commandsFor(commands, soundFileFor(soundDirectory, phase), environment), fallback = () => {
|
|
282
|
+
writeFallback(FALLBACK_SOUNDS[phase]);
|
|
283
|
+
};
|
|
284
|
+
tracker.startOperation(completionNotice, (operation) => runSoundCommands(operation, tracker, candidates, fallback));
|
|
285
|
+
},
|
|
286
|
+
shutdown: (options) => tracker.shutdown(options)
|
|
287
|
+
});
|
|
288
|
+
//#endregion
|
|
289
|
+
//#region src/diagnostics/escape.ts
|
|
290
|
+
const BACKSLASH = "\\";
|
|
291
|
+
const BIDIRECTIONAL_PATTERN = /[\u061C\u200B\u200E\u200F\u2028-\u202E\u2066-\u206F\uFEFF]/u;
|
|
292
|
+
const CONTROL_HEX_LENGTH = 2;
|
|
293
|
+
const CONTROL_PATTERN = /[\u0000-\u001F\u007F-\u009F]/u;
|
|
294
|
+
const EMPTY_CODE_POINT = 0;
|
|
295
|
+
const FIRST_INDEX = 0;
|
|
296
|
+
const HEXADECIMAL_RADIX = 16;
|
|
297
|
+
const HEX_PAD_CHARACTER = "0";
|
|
298
|
+
const MAXIMUM_CODE_POINTS = 160;
|
|
299
|
+
const QUOTATION_MARK = "\"";
|
|
300
|
+
const SURROGATE_HEX_LENGTH = 4;
|
|
301
|
+
const SURROGATE_PATTERN = /[\uD800-\uDFFF]/u;
|
|
302
|
+
const TRUNCATION_MARK = "…";
|
|
303
|
+
const UNPADDED_HEX_LENGTH = 0;
|
|
304
|
+
const characterToHexadecimal = (character, length) => (character.codePointAt(FIRST_INDEX) ?? EMPTY_CODE_POINT).toString(HEXADECIMAL_RADIX).toUpperCase().padStart(length, HEX_PAD_CHARACTER);
|
|
305
|
+
const escapeUnsafeCharacter = (character) => {
|
|
306
|
+
if (CONTROL_PATTERN.test(character)) return String.raw`\x${characterToHexadecimal(character, CONTROL_HEX_LENGTH)}`;
|
|
307
|
+
if (BIDIRECTIONAL_PATTERN.test(character)) return String.raw`\u{${characterToHexadecimal(character, UNPADDED_HEX_LENGTH)}}`;
|
|
308
|
+
if (SURROGATE_PATTERN.test(character)) return String.raw`\u${characterToHexadecimal(character, SURROGATE_HEX_LENGTH)}`;
|
|
309
|
+
return character;
|
|
310
|
+
};
|
|
311
|
+
const escapeCharacter = (character) => {
|
|
312
|
+
if (character === BACKSLASH) return String.raw`\\`;
|
|
313
|
+
if (character === QUOTATION_MARK) return String.raw`\"`;
|
|
314
|
+
return escapeUnsafeCharacter(character);
|
|
315
|
+
};
|
|
316
|
+
const escapeDiagnostic = (value) => {
|
|
317
|
+
const codePoints = [...value], escaped = codePoints.slice(FIRST_INDEX, MAXIMUM_CODE_POINTS).map((character) => escapeCharacter(character)).join("");
|
|
318
|
+
if (codePoints.length > MAXIMUM_CODE_POINTS) return `${QUOTATION_MARK}${escaped}${TRUNCATION_MARK}${QUOTATION_MARK}`;
|
|
319
|
+
return `${QUOTATION_MARK}${escaped}${QUOTATION_MARK}`;
|
|
320
|
+
};
|
|
321
|
+
//#endregion
|
|
322
|
+
//#region src/platform/commands.ts
|
|
323
|
+
const AFPLAY_PATH = "/usr/bin/afplay";
|
|
324
|
+
const DEFAULT_LINUX_PATH = [
|
|
325
|
+
"/usr/local/bin",
|
|
326
|
+
"/usr/bin",
|
|
327
|
+
"/bin"
|
|
328
|
+
];
|
|
329
|
+
const NODE_MODULES_SEGMENT = "node_modules";
|
|
330
|
+
const BIN_SEGMENT = ".bin";
|
|
331
|
+
const NEXT_SEGMENT_OFFSET = 1;
|
|
332
|
+
const POWERSHELL_SEGMENTS = [
|
|
333
|
+
"System32",
|
|
334
|
+
"WindowsPowerShell",
|
|
335
|
+
"v1.0",
|
|
336
|
+
"powershell.exe"
|
|
337
|
+
];
|
|
338
|
+
const isExecutableFile = (target) => {
|
|
339
|
+
try {
|
|
340
|
+
if (!statSync(target).isFile()) return false;
|
|
341
|
+
accessSync(target, constants.X_OK);
|
|
342
|
+
return true;
|
|
343
|
+
} catch {
|
|
344
|
+
return false;
|
|
345
|
+
}
|
|
346
|
+
};
|
|
347
|
+
const nodeFileSystem = {
|
|
348
|
+
isExecutableFile,
|
|
349
|
+
realpath: (target) => realpathSync(target)
|
|
350
|
+
};
|
|
351
|
+
const comparablePath = (value, context) => {
|
|
352
|
+
const normalized = context.pathApi.normalize(value);
|
|
353
|
+
if (context.caseInsensitive) return normalized.toLowerCase();
|
|
354
|
+
return normalized;
|
|
355
|
+
};
|
|
356
|
+
const isWithin = (parent, child, context) => {
|
|
357
|
+
const relative = context.pathApi.relative(comparablePath(parent, context), comparablePath(child, context));
|
|
358
|
+
return relative === "" || !relative.startsWith(`..${context.pathApi.sep}`) && relative !== ".." && !context.pathApi.isAbsolute(relative);
|
|
359
|
+
};
|
|
360
|
+
const containsNodeModulesBin = (value, context) => {
|
|
361
|
+
const segments = comparablePath(value, context).split(context.pathApi.sep).filter((segment) => segment !== "");
|
|
362
|
+
return segments.some((segment, index) => segment === NODE_MODULES_SEGMENT && segments[index + NEXT_SEGMENT_OFFSET] === BIN_SEGMENT);
|
|
363
|
+
};
|
|
364
|
+
const isExcluded = (value, context) => containsNodeModulesBin(value, context) || context.excludedDirectories.some((directory) => isWithin(directory, value, context));
|
|
365
|
+
const tryRealpath = (target, context) => {
|
|
366
|
+
try {
|
|
367
|
+
return context.fileSystem.realpath(target);
|
|
368
|
+
} catch {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
};
|
|
372
|
+
const excludedDirectoriesFor = (cwd, fileSystem) => {
|
|
373
|
+
try {
|
|
374
|
+
return [cwd, fileSystem.realpath(cwd)];
|
|
375
|
+
} catch {
|
|
376
|
+
return [cwd];
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
const commandInDirectory = (directory, name, context) => {
|
|
380
|
+
if (!context.pathApi.isAbsolute(directory) || isExcluded(directory, context)) return;
|
|
381
|
+
const realDirectory = tryRealpath(directory, context);
|
|
382
|
+
if (realDirectory === void 0 || isExcluded(realDirectory, context)) return;
|
|
383
|
+
const candidate = tryRealpath(context.pathApi.join(realDirectory, name), context);
|
|
384
|
+
if (candidate === void 0 || isExcluded(candidate, context) || !context.fileSystem.isExecutableFile(candidate)) return;
|
|
385
|
+
return candidate;
|
|
386
|
+
};
|
|
387
|
+
const findInPath = (name, environment, context) => {
|
|
388
|
+
const directories = environment.PATH?.split(path.posix.delimiter) ?? DEFAULT_LINUX_PATH;
|
|
389
|
+
for (const directory of directories) {
|
|
390
|
+
const resolved = commandInDirectory(directory, name, context);
|
|
391
|
+
if (resolved !== void 0) return resolved;
|
|
392
|
+
}
|
|
393
|
+
};
|
|
394
|
+
const findWindowsPowerShell = (environment, context) => {
|
|
395
|
+
const systemRoot = environment.SystemRoot;
|
|
396
|
+
if (systemRoot === void 0 || !path.win32.isAbsolute(systemRoot)) return;
|
|
397
|
+
const realSystemRoot = tryRealpath(systemRoot, context);
|
|
398
|
+
if (realSystemRoot === void 0) return;
|
|
399
|
+
const candidate = tryRealpath(path.win32.join(realSystemRoot, ...POWERSHELL_SEGMENTS), context);
|
|
400
|
+
if (candidate === void 0 || isExcluded(candidate, context) || !context.fileSystem.isExecutableFile(candidate)) return;
|
|
401
|
+
return candidate;
|
|
402
|
+
};
|
|
403
|
+
const resolvePlayerCommands = ({ cwd = process$1.cwd(), environment = process$1.env, fileSystem = nodeFileSystem, platform = process$1.platform } = {}) => {
|
|
404
|
+
switch (platform) {
|
|
405
|
+
case "darwin":
|
|
406
|
+
if (fileSystem.isExecutableFile(AFPLAY_PATH)) return { afplay: AFPLAY_PATH };
|
|
407
|
+
return {};
|
|
408
|
+
case "linux": {
|
|
409
|
+
const context = {
|
|
410
|
+
caseInsensitive: false,
|
|
411
|
+
excludedDirectories: excludedDirectoriesFor(cwd, fileSystem),
|
|
412
|
+
fileSystem,
|
|
413
|
+
pathApi: path.posix
|
|
414
|
+
};
|
|
415
|
+
return {
|
|
416
|
+
aplay: findInPath("aplay", environment, context),
|
|
417
|
+
paplay: findInPath("paplay", environment, context)
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
case "win32": {
|
|
421
|
+
const context = {
|
|
422
|
+
caseInsensitive: true,
|
|
423
|
+
excludedDirectories: excludedDirectoriesFor(cwd, fileSystem),
|
|
424
|
+
fileSystem,
|
|
425
|
+
pathApi: path.win32
|
|
426
|
+
};
|
|
427
|
+
return { powershell: findWindowsPowerShell(environment, context) };
|
|
428
|
+
}
|
|
429
|
+
default: return {};
|
|
430
|
+
}
|
|
431
|
+
};
|
|
432
|
+
//#endregion
|
|
433
|
+
//#region src/timer/state.ts
|
|
434
|
+
const FIRST_CYCLE$1 = 1;
|
|
435
|
+
const COUNT_INCREMENT = 1;
|
|
436
|
+
const NO_TIME$1 = 0;
|
|
437
|
+
const durationFor$1 = (phase, settings) => {
|
|
438
|
+
if (phase === "work") return settings.workDurationMs;
|
|
439
|
+
return settings.breakDurationMs;
|
|
440
|
+
};
|
|
441
|
+
const startPhase = ({ completedPomodoros, cycle, phase }, now, settings) => ({
|
|
442
|
+
completedPomodoros,
|
|
443
|
+
cycle,
|
|
444
|
+
endsAt: now + durationFor$1(phase, settings),
|
|
445
|
+
paused: false,
|
|
446
|
+
phase,
|
|
447
|
+
remainingMs: NO_TIME$1
|
|
448
|
+
});
|
|
449
|
+
const createInitialState = (settings, startedAt) => startPhase({
|
|
450
|
+
completedPomodoros: 0,
|
|
451
|
+
cycle: FIRST_CYCLE$1,
|
|
452
|
+
phase: "work"
|
|
453
|
+
}, startedAt, settings);
|
|
454
|
+
const nextPhase = ({ completedPomodoros, cycle, phase }, now, settings) => {
|
|
455
|
+
if (phase === "work") return startPhase({
|
|
456
|
+
completedPomodoros,
|
|
457
|
+
cycle,
|
|
458
|
+
phase: "break"
|
|
459
|
+
}, now, settings);
|
|
460
|
+
if (cycle < settings.loopCount) return startPhase({
|
|
461
|
+
completedPomodoros,
|
|
462
|
+
cycle: cycle + COUNT_INCREMENT,
|
|
463
|
+
phase: "work"
|
|
464
|
+
}, now, settings);
|
|
465
|
+
};
|
|
466
|
+
const completedPomodorosAfterTimeout = (state) => {
|
|
467
|
+
if (state.phase === "work") return state.completedPomodoros + COUNT_INCREMENT;
|
|
468
|
+
return state.completedPomodoros;
|
|
469
|
+
};
|
|
470
|
+
const transitionFromTimeout = (state, now, settings) => {
|
|
471
|
+
const next = nextPhase({
|
|
472
|
+
completedPomodoros: completedPomodorosAfterTimeout(state),
|
|
473
|
+
cycle: state.cycle,
|
|
474
|
+
phase: state.phase
|
|
475
|
+
}, now, settings);
|
|
476
|
+
if (next === void 0) return {
|
|
477
|
+
completedPhase: state.phase,
|
|
478
|
+
final: true,
|
|
479
|
+
state
|
|
480
|
+
};
|
|
481
|
+
return {
|
|
482
|
+
completedPhase: state.phase,
|
|
483
|
+
final: false,
|
|
484
|
+
state: next
|
|
485
|
+
};
|
|
486
|
+
};
|
|
487
|
+
const transitionFromSkip = (state, now, settings) => {
|
|
488
|
+
const next = nextPhase(state, now, settings);
|
|
489
|
+
if (next === void 0) return {
|
|
490
|
+
final: true,
|
|
491
|
+
skippedPhase: state.phase,
|
|
492
|
+
state
|
|
493
|
+
};
|
|
494
|
+
return {
|
|
495
|
+
final: false,
|
|
496
|
+
skippedPhase: state.phase,
|
|
497
|
+
state: next
|
|
498
|
+
};
|
|
499
|
+
};
|
|
500
|
+
const remainingMsAt = (state, now) => {
|
|
501
|
+
if (state.paused) return state.remainingMs;
|
|
502
|
+
return Math.max(NO_TIME$1, state.endsAt - now);
|
|
503
|
+
};
|
|
504
|
+
const togglePause = (state, now) => {
|
|
505
|
+
if (state.paused) return {
|
|
506
|
+
completedPomodoros: state.completedPomodoros,
|
|
507
|
+
cycle: state.cycle,
|
|
508
|
+
endsAt: now + state.remainingMs,
|
|
509
|
+
paused: false,
|
|
510
|
+
phase: state.phase,
|
|
511
|
+
remainingMs: NO_TIME$1
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
completedPomodoros: state.completedPomodoros,
|
|
515
|
+
cycle: state.cycle,
|
|
516
|
+
endsAt: state.endsAt,
|
|
517
|
+
paused: true,
|
|
518
|
+
phase: state.phase,
|
|
519
|
+
remainingMs: remainingMsAt(state, now)
|
|
520
|
+
};
|
|
521
|
+
};
|
|
522
|
+
const MILLISECONDS_PER_MINUTE = 6e4;
|
|
523
|
+
const DEFAULT_BREAK_DURATION_MS = 5 * MILLISECONDS_PER_MINUTE;
|
|
524
|
+
const DEFAULT_WORK_DURATION_MS = 15 * MILLISECONDS_PER_MINUTE;
|
|
525
|
+
const sleepWithTimer = (durationMs, signal) => setTimeout$1(durationMs, void 0, { signal });
|
|
526
|
+
const combineSignals = (signal, internal) => {
|
|
527
|
+
if (signal === void 0) return internal;
|
|
528
|
+
return AbortSignal.any([signal, internal]);
|
|
529
|
+
};
|
|
530
|
+
const waitForTick = async (sleep, signal) => {
|
|
531
|
+
if (signal?.aborted) return false;
|
|
532
|
+
try {
|
|
533
|
+
await sleep(250, signal);
|
|
534
|
+
} catch (error) {
|
|
535
|
+
if (signal?.aborted) return false;
|
|
536
|
+
throw error;
|
|
537
|
+
}
|
|
538
|
+
return signal?.aborted !== true;
|
|
539
|
+
};
|
|
540
|
+
const runPomodoroTimer = async ({ workDurationMs = DEFAULT_WORK_DURATION_MS, breakDurationMs = DEFAULT_BREAK_DURATION_MS, loopCount, now = Date.now, sleep = sleepWithTimer, signal, onPhaseStarted, onPhaseCompleted, onPhaseSkipped, onTick, bindControls }) => {
|
|
541
|
+
const settings = {
|
|
542
|
+
breakDurationMs,
|
|
543
|
+
loopCount,
|
|
544
|
+
workDurationMs
|
|
545
|
+
}, startedAt = now(), finishController = new AbortController(), tickSignal = combineSignals(signal, finishController.signal);
|
|
546
|
+
let state = createInitialState(settings, startedAt), finished = false;
|
|
547
|
+
/** 最後の休憩の終了でタイマーを完了し、待機中のtickを打ち切ります。 */
|
|
548
|
+
const finish = () => {
|
|
549
|
+
finished = true;
|
|
550
|
+
finishController.abort();
|
|
551
|
+
}, notifyStarted = () => {
|
|
552
|
+
onPhaseStarted?.(state.phase, durationFor$1(state.phase, settings));
|
|
553
|
+
}, settleExpiredPhase = (currentTime) => {
|
|
554
|
+
if (finished || state.paused || currentTime < state.endsAt) return finished;
|
|
555
|
+
const transition = transitionFromTimeout(state, currentTime, settings);
|
|
556
|
+
({state} = transition);
|
|
557
|
+
onPhaseCompleted?.(transition.completedPhase, { final: transition.final });
|
|
558
|
+
if (transition.final) {
|
|
559
|
+
finish();
|
|
560
|
+
return true;
|
|
561
|
+
}
|
|
562
|
+
notifyStarted();
|
|
563
|
+
return false;
|
|
564
|
+
}, applyManualAction = (action) => {
|
|
565
|
+
if (finished || tickSignal.aborted) return finished;
|
|
566
|
+
const currentTime = now();
|
|
567
|
+
if (settleExpiredPhase(currentTime)) return true;
|
|
568
|
+
action(currentTime);
|
|
569
|
+
if (!finished) onTick?.(state, currentTime);
|
|
570
|
+
return finished;
|
|
571
|
+
};
|
|
572
|
+
bindControls?.({
|
|
573
|
+
settle: () => applyManualAction(() => {}),
|
|
574
|
+
skip: () => {
|
|
575
|
+
applyManualAction((currentTime) => {
|
|
576
|
+
const transition = transitionFromSkip(state, currentTime, settings);
|
|
577
|
+
({state} = transition);
|
|
578
|
+
onPhaseSkipped?.(transition.skippedPhase);
|
|
579
|
+
if (transition.final) {
|
|
580
|
+
finish();
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
notifyStarted();
|
|
584
|
+
});
|
|
585
|
+
},
|
|
586
|
+
togglePause: () => {
|
|
587
|
+
applyManualAction((currentTime) => {
|
|
588
|
+
state = togglePause(state, currentTime);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
});
|
|
592
|
+
notifyStarted();
|
|
593
|
+
onTick?.(state, startedAt);
|
|
594
|
+
for (;;) {
|
|
595
|
+
if (!await waitForTick(sleep, tickSignal)) return {
|
|
596
|
+
completed: finished,
|
|
597
|
+
completedPomodoros: state.completedPomodoros
|
|
598
|
+
};
|
|
599
|
+
const currentTime = now();
|
|
600
|
+
if (settleExpiredPhase(currentTime)) return {
|
|
601
|
+
completed: true,
|
|
602
|
+
completedPomodoros: state.completedPomodoros
|
|
603
|
+
};
|
|
604
|
+
onTick?.(state, currentTime);
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
//#endregion
|
|
608
|
+
//#region src/terminal/output.ts
|
|
609
|
+
/** 出力先が閉じたパイプであることを示すエラーコードです。 */
|
|
610
|
+
const BROKEN_PIPE_CODE = "EPIPE";
|
|
611
|
+
const isBrokenPipe = (error) => typeof error === "object" && error !== null && Reflect.get(error, "code") === BROKEN_PIPE_CODE;
|
|
612
|
+
const createStandardOutput = (onFailure, stream = process$1.stdout) => {
|
|
613
|
+
let unavailable = false;
|
|
614
|
+
const fail = (error) => {
|
|
615
|
+
if (unavailable) return;
|
|
616
|
+
unavailable = true;
|
|
617
|
+
onFailure(error);
|
|
618
|
+
};
|
|
619
|
+
stream.on("error", fail);
|
|
620
|
+
return (output) => {
|
|
621
|
+
if (unavailable) return;
|
|
622
|
+
try {
|
|
623
|
+
stream.write(output);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
fail(error);
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
};
|
|
629
|
+
const OPTIONS = [
|
|
630
|
+
{
|
|
631
|
+
description: `Work duration in minutes (1-1440, default: 15)`,
|
|
632
|
+
name: "--work",
|
|
633
|
+
valueName: "<minutes>"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
description: `Break duration in minutes (1-1440, default: 5)`,
|
|
637
|
+
name: "--break",
|
|
638
|
+
valueName: "<minutes>"
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
description: `Work-break repetitions (positive integer, default: 3)`,
|
|
642
|
+
name: "--loop",
|
|
643
|
+
valueName: "<count>"
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
alias: "-h",
|
|
647
|
+
description: "Print help",
|
|
648
|
+
name: "--help"
|
|
649
|
+
}
|
|
650
|
+
];
|
|
651
|
+
//#endregion
|
|
652
|
+
//#region src/terminal/input.ts
|
|
653
|
+
const CHOICES = ["OK", "NG"];
|
|
654
|
+
const ENTER_KEY_NAMES = /* @__PURE__ */ new Set(["enter", "return"]);
|
|
655
|
+
const FIRST_CHOICE_INDEX = 0;
|
|
656
|
+
const KEYPRESS_EVENT_KEY_INDEX = 1;
|
|
657
|
+
const MOVE_KEY_NAMES = /* @__PURE__ */ new Set(["down", "up"]);
|
|
658
|
+
const NEXT_INDEX_OFFSET = 1;
|
|
659
|
+
const REDRAW_SEQUENCE = `\r\u001B[${CHOICES.length}A\u001B[J`;
|
|
660
|
+
const STDIN_CLOSE_EVENTS = ["end", "close"];
|
|
661
|
+
const SELECTION_QUESTION = "Start working? (Use ↑/↓ to select, Enter to confirm)";
|
|
662
|
+
const keyFrom = (value) => {
|
|
663
|
+
let key = {}, normalizedName = "";
|
|
664
|
+
if (typeof value === "object" && value !== null) key = value;
|
|
665
|
+
const ctrl = Reflect.get(key, "ctrl"), name = Reflect.get(key, "name");
|
|
666
|
+
if (typeof name === "string") normalizedName = name;
|
|
667
|
+
return {
|
|
668
|
+
ctrl: ctrl === true,
|
|
669
|
+
name: normalizedName
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
const createSelectionTerminal = ({ input = process$1.stdin, signal, write = (output) => {
|
|
673
|
+
process$1.stdout.write(output);
|
|
674
|
+
} } = {}) => {
|
|
675
|
+
emitKeypressEvents(input);
|
|
676
|
+
const inputWasFlowing = input.readableFlowing === true, keypressEvents = on(input, "keypress", { close: STDIN_CLOSE_EVENTS }), rawModeWasEnabled = input.isRaw === true, supportsRawMode = input.isTTY === true && typeof input.setRawMode === "function", stopListening = () => {
|
|
677
|
+
keypressEvents.return?.();
|
|
678
|
+
};
|
|
679
|
+
let restored = false;
|
|
680
|
+
signal?.addEventListener("abort", stopListening, { once: true });
|
|
681
|
+
if (signal?.aborted) stopListening();
|
|
682
|
+
if (supportsRawMode && !rawModeWasEnabled) input.setRawMode?.(true);
|
|
683
|
+
if (!inputWasFlowing) input.resume();
|
|
684
|
+
return {
|
|
685
|
+
nextKey: async () => {
|
|
686
|
+
const event = await keypressEvents.next();
|
|
687
|
+
if (event.done) return {
|
|
688
|
+
ctrl: true,
|
|
689
|
+
name: "c"
|
|
690
|
+
};
|
|
691
|
+
return keyFrom(event.value.at(KEYPRESS_EVENT_KEY_INDEX));
|
|
692
|
+
},
|
|
693
|
+
restore: async () => {
|
|
694
|
+
if (restored) return;
|
|
695
|
+
restored = true;
|
|
696
|
+
signal?.removeEventListener("abort", stopListening);
|
|
697
|
+
await keypressEvents.return?.();
|
|
698
|
+
if (supportsRawMode && !rawModeWasEnabled) input.setRawMode?.(false);
|
|
699
|
+
if (!inputWasFlowing) input.pause();
|
|
700
|
+
},
|
|
701
|
+
write
|
|
702
|
+
};
|
|
703
|
+
};
|
|
704
|
+
const markerFor = (selected) => {
|
|
705
|
+
if (selected) return "❯";
|
|
706
|
+
return " ";
|
|
707
|
+
};
|
|
708
|
+
const renderSelection = (selectedIndex) => [SELECTION_QUESTION, ...CHOICES.map((choice, index) => `${markerFor(index === selectedIndex)} ${choice}`)].join("\n");
|
|
709
|
+
const nextChoiceIndex = (selectedIndex) => {
|
|
710
|
+
if (selectedIndex === FIRST_CHOICE_INDEX) return CHOICES.length - NEXT_INDEX_OFFSET;
|
|
711
|
+
return FIRST_CHOICE_INDEX;
|
|
712
|
+
};
|
|
713
|
+
const handleKey = (key, selectedIndex, terminal) => {
|
|
714
|
+
if (key.ctrl && key.name === "c") {
|
|
715
|
+
terminal.write("\n");
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
if (MOVE_KEY_NAMES.has(key.name)) {
|
|
719
|
+
const newSelectedIndex = nextChoiceIndex(selectedIndex);
|
|
720
|
+
terminal.write(`${REDRAW_SEQUENCE}${renderSelection(newSelectedIndex)}`);
|
|
721
|
+
return newSelectedIndex;
|
|
722
|
+
}
|
|
723
|
+
if (ENTER_KEY_NAMES.has(key.name)) {
|
|
724
|
+
terminal.write("\n");
|
|
725
|
+
return CHOICES[selectedIndex] === "OK";
|
|
726
|
+
}
|
|
727
|
+
return selectedIndex;
|
|
728
|
+
};
|
|
729
|
+
const confirmWorkStart = async ({ createTerminal = createSelectionTerminal, ...terminalParameters } = {}) => {
|
|
730
|
+
const terminal = createTerminal(terminalParameters);
|
|
731
|
+
let selectedIndex = FIRST_CHOICE_INDEX;
|
|
732
|
+
try {
|
|
733
|
+
terminal.write(renderSelection(selectedIndex));
|
|
734
|
+
for (;;) {
|
|
735
|
+
const key = await terminal.nextKey(), selection = handleKey(key, selectedIndex, terminal);
|
|
736
|
+
if (typeof selection === "boolean") return selection;
|
|
737
|
+
selectedIndex = selection;
|
|
738
|
+
}
|
|
739
|
+
} finally {
|
|
740
|
+
await terminal.restore();
|
|
741
|
+
}
|
|
742
|
+
};
|
|
743
|
+
//#endregion
|
|
744
|
+
//#region src/terminal/constants.ts
|
|
745
|
+
/**
|
|
746
|
+
* ロゴが収まらない狭い端末で、ロゴの代わりに表示する見出しです。
|
|
747
|
+
*/
|
|
748
|
+
const COMPACT_LOGO = `
|
|
749
|
+
🍅 pomotty
|
|
750
|
+
`;
|
|
751
|
+
const LOGO = `
|
|
752
|
+
██████╗ ██████╗ ███╗ ███╗ ██████╗ ████████╗████████╗██╗ ██╗
|
|
753
|
+
██╔══██╗██╔═══██╗████╗ ████║██╔═══██╗╚══██╔══╝╚══██╔══╝╚██╗ ██╔╝
|
|
754
|
+
██████╔╝██║ ██║██╔████╔██║██║ ██║ ██║ ██║ ╚████╔╝
|
|
755
|
+
██╔═══╝ ██║ ██║██║╚██╔╝██║██║ ██║ ██║ ██║ ╚██╔╝
|
|
756
|
+
██║ ╚██████╔╝██║ ╚═╝ ██║╚██████╔╝ ██║ ██║ ██║
|
|
757
|
+
╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚═╝
|
|
758
|
+
`;
|
|
759
|
+
//#endregion
|
|
760
|
+
//#region src/terminal/frame-line.ts
|
|
761
|
+
const ELLIPSIS = "…";
|
|
762
|
+
const NO_WIDTH = 0;
|
|
763
|
+
const SINGLE_WIDTH = 1;
|
|
764
|
+
const DOUBLE_WIDTH = 2;
|
|
765
|
+
const WIDE_CHARACTER = /[\u1100-\u115F\u2615\u2E80-\uA4CF\uAC00-\uD7A3\uF900-\uFAFF\uFE30-\uFE4F\uFF00-\uFF60\uFFE0-\uFFE6\u{1F300}-\u{1FAFF}]/u;
|
|
766
|
+
const ZERO_WIDTH_CHARACTERS = /* @__PURE__ */ new Set([
|
|
767
|
+
"",
|
|
768
|
+
"︎",
|
|
769
|
+
"️"
|
|
770
|
+
]);
|
|
771
|
+
const SGR_DEFAULT_FOREGROUND = "\x1B[39m";
|
|
772
|
+
const SGR_TONES = {
|
|
773
|
+
break: "\x1B[32m",
|
|
774
|
+
work: "\x1B[31m"
|
|
775
|
+
};
|
|
776
|
+
const characterWidth = (character) => {
|
|
777
|
+
if (ZERO_WIDTH_CHARACTERS.has(character)) return NO_WIDTH;
|
|
778
|
+
if (WIDE_CHARACTER.test(character)) return DOUBLE_WIDTH;
|
|
779
|
+
return SINGLE_WIDTH;
|
|
780
|
+
};
|
|
781
|
+
const cellWidth = (text) => [...text].reduce((width, character) => width + characterWidth(character), NO_WIDTH);
|
|
782
|
+
const lineWidth = (line) => line.reduce((width, segment) => width + cellWidth(segment.text), NO_WIDTH);
|
|
783
|
+
const takeWithin = (text, maxWidth) => {
|
|
784
|
+
let taken = "", used = NO_WIDTH;
|
|
785
|
+
for (const character of text) {
|
|
786
|
+
used += characterWidth(character);
|
|
787
|
+
if (used > maxWidth) break;
|
|
788
|
+
taken += character;
|
|
789
|
+
}
|
|
790
|
+
return taken;
|
|
791
|
+
};
|
|
792
|
+
const takeSegmentsWithin = (line, maxWidth) => {
|
|
793
|
+
const kept = [];
|
|
794
|
+
let remaining = maxWidth;
|
|
795
|
+
for (const { text, tone } of line) {
|
|
796
|
+
const taken = takeWithin(text, remaining);
|
|
797
|
+
if (taken !== "") kept.push({
|
|
798
|
+
text: taken,
|
|
799
|
+
tone
|
|
800
|
+
});
|
|
801
|
+
if (taken !== text) break;
|
|
802
|
+
remaining -= cellWidth(taken);
|
|
803
|
+
}
|
|
804
|
+
return kept;
|
|
805
|
+
};
|
|
806
|
+
const truncateLine = (line, maxWidth) => {
|
|
807
|
+
if (lineWidth(line) <= maxWidth) return line;
|
|
808
|
+
return [...takeSegmentsWithin(line, maxWidth - cellWidth(ELLIPSIS)), { text: ELLIPSIS }];
|
|
809
|
+
};
|
|
810
|
+
const stringifySegment = ({ text, tone }, color) => {
|
|
811
|
+
if (!color || !tone || text === "") return text;
|
|
812
|
+
return `${SGR_TONES[tone]}${text}${SGR_DEFAULT_FOREGROUND}`;
|
|
813
|
+
};
|
|
814
|
+
const stringifyFrameLine = (line, color) => line.map((segment) => stringifySegment(segment, color)).join("").trimEnd();
|
|
815
|
+
const isColorEnabled = (env) => (env.NO_COLOR ?? "") === "";
|
|
816
|
+
//#endregion
|
|
817
|
+
//#region src/terminal/render.ts
|
|
818
|
+
const CLI_OPTION_INDEX = 2;
|
|
819
|
+
const LOGO_WIDTH = Math.max(...LOGO.split("\n").map((line) => cellWidth(line)));
|
|
820
|
+
const RIGHT_MARGIN$1 = 1;
|
|
821
|
+
const canShowLogo = (columns) => !columns || LOGO_WIDTH <= columns - RIGHT_MARGIN$1;
|
|
822
|
+
const createCliOutput = ({ columns, options, optionName = process.argv.at(CLI_OPTION_INDEX) }) => {
|
|
823
|
+
const option = options.find(({ alias, name }) => alias === optionName || name === optionName);
|
|
824
|
+
if (!option && canShowLogo(columns)) return `${LOGO}\n`;
|
|
825
|
+
if (!option) return `${COMPACT_LOGO}\n`;
|
|
826
|
+
return [
|
|
827
|
+
"Pomotty CLI",
|
|
828
|
+
"",
|
|
829
|
+
"Usage: pomotty [OPTIONS]",
|
|
830
|
+
"",
|
|
831
|
+
"Options:",
|
|
832
|
+
...options.flatMap(({ alias, description, name, valueName }) => {
|
|
833
|
+
return [` ${[alias, [name, valueName].filter(Boolean).join(" ")].filter(Boolean).join(", ")}`, ` ${description}`];
|
|
834
|
+
}),
|
|
835
|
+
""
|
|
836
|
+
].join("\n");
|
|
837
|
+
};
|
|
838
|
+
//#endregion
|
|
839
|
+
//#region src/terminal/inline-renderer.ts
|
|
840
|
+
const CSI = "\x1B[";
|
|
841
|
+
const NO_ROWS = 0;
|
|
842
|
+
const CLEAR_LINE = `${CSI}2K`;
|
|
843
|
+
const cursorUp = (rows) => {
|
|
844
|
+
if (rows <= NO_ROWS) return "";
|
|
845
|
+
return `${CSI}${rows}A`;
|
|
846
|
+
};
|
|
847
|
+
const createInlineRenderer = (write) => {
|
|
848
|
+
let previous, renderedRows = NO_ROWS;
|
|
849
|
+
const eraseSequence = () => `${cursorUp(renderedRows)}${`${CLEAR_LINE}\n`.repeat(renderedRows)}${cursorUp(renderedRows)}`;
|
|
850
|
+
return {
|
|
851
|
+
log: (output) => {
|
|
852
|
+
write(`${eraseSequence()}${output}`);
|
|
853
|
+
previous = void 0;
|
|
854
|
+
renderedRows = NO_ROWS;
|
|
855
|
+
},
|
|
856
|
+
release: () => {
|
|
857
|
+
previous = void 0;
|
|
858
|
+
renderedRows = NO_ROWS;
|
|
859
|
+
},
|
|
860
|
+
render: (lines) => {
|
|
861
|
+
const frame = lines.join("\n");
|
|
862
|
+
if (frame === previous) return;
|
|
863
|
+
const leftoverRows = Math.max(NO_ROWS, renderedRows - lines.length);
|
|
864
|
+
write([
|
|
865
|
+
cursorUp(renderedRows),
|
|
866
|
+
...lines.map((line) => `${CLEAR_LINE}${line}\n`),
|
|
867
|
+
`${CLEAR_LINE}\n`.repeat(leftoverRows),
|
|
868
|
+
cursorUp(leftoverRows)
|
|
869
|
+
].join(""));
|
|
870
|
+
previous = frame;
|
|
871
|
+
renderedRows = lines.length;
|
|
872
|
+
}
|
|
873
|
+
};
|
|
874
|
+
};
|
|
875
|
+
//#endregion
|
|
876
|
+
//#region src/terminal/terminal-guard.ts
|
|
877
|
+
const HIDE_CURSOR = "\x1B[?25l";
|
|
878
|
+
const SHOW_CURSOR = "\x1B[?25h";
|
|
879
|
+
const TERMINATION_SIGNALS = [
|
|
880
|
+
"SIGHUP",
|
|
881
|
+
"SIGINT",
|
|
882
|
+
"SIGTERM"
|
|
883
|
+
];
|
|
884
|
+
const createSyncCursorRestorer = (terminal) => () => {
|
|
885
|
+
if (terminal.fd === void 0) return;
|
|
886
|
+
try {
|
|
887
|
+
writeSync(terminal.fd, SHOW_CURSOR);
|
|
888
|
+
} catch {}
|
|
889
|
+
};
|
|
890
|
+
const createTerminalGuard = ({ onSignal, processEvents, terminal, write }) => {
|
|
891
|
+
const restoreCursorSync = createSyncCursorRestorer(terminal), signalHandlers = TERMINATION_SIGNALS.map((signal) => {
|
|
892
|
+
const handler = () => {
|
|
893
|
+
onSignal(signal);
|
|
894
|
+
};
|
|
895
|
+
processEvents.on(signal, handler);
|
|
896
|
+
return [signal, handler];
|
|
897
|
+
});
|
|
898
|
+
let cursorHidden = false, released = false;
|
|
899
|
+
return {
|
|
900
|
+
hideCursor: () => {
|
|
901
|
+
if (cursorHidden || released) return;
|
|
902
|
+
cursorHidden = true;
|
|
903
|
+
processEvents.on("exit", restoreCursorSync);
|
|
904
|
+
write(HIDE_CURSOR);
|
|
905
|
+
},
|
|
906
|
+
release: () => {
|
|
907
|
+
if (released) return;
|
|
908
|
+
released = true;
|
|
909
|
+
for (const [signal, handler] of signalHandlers) processEvents.off(signal, handler);
|
|
910
|
+
if (cursorHidden) {
|
|
911
|
+
write(SHOW_CURSOR);
|
|
912
|
+
processEvents.off("exit", restoreCursorSync);
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
};
|
|
916
|
+
};
|
|
917
|
+
//#endregion
|
|
918
|
+
//#region src/terminal/clock.ts
|
|
919
|
+
const MILLISECONDS_PER_SECOND = 1e3;
|
|
920
|
+
const SECONDS_PER_MINUTE = 60;
|
|
921
|
+
const SECONDS_PER_HOUR = 3600;
|
|
922
|
+
const PERCENT$1 = 100;
|
|
923
|
+
const TIME_PART_WIDTH = 2;
|
|
924
|
+
const NO_TIME = 0;
|
|
925
|
+
const MIN_RATIO = 0;
|
|
926
|
+
const MAX_RATIO = 1;
|
|
927
|
+
const twoDigits = (value) => String(value).padStart(TIME_PART_WIDTH, "0");
|
|
928
|
+
const formatClock = (seconds) => {
|
|
929
|
+
const hours = Math.floor(seconds / SECONDS_PER_HOUR), minutes = Math.floor(seconds % SECONDS_PER_HOUR / SECONDS_PER_MINUTE), restSeconds = seconds % SECONDS_PER_MINUTE;
|
|
930
|
+
if (hours === NO_TIME) return `${minutes}:${twoDigits(restSeconds)}`;
|
|
931
|
+
return `${hours}:${twoDigits(minutes)}:${twoDigits(restSeconds)}`;
|
|
932
|
+
};
|
|
933
|
+
const toDisplaySeconds = (milliseconds) => Math.ceil(milliseconds / MILLISECONDS_PER_SECOND);
|
|
934
|
+
const formatDuration = (milliseconds) => formatClock(toDisplaySeconds(milliseconds));
|
|
935
|
+
const durationFor = (phase, settings) => {
|
|
936
|
+
if (phase === "work") return settings.workDurationMs;
|
|
937
|
+
return settings.breakDurationMs;
|
|
938
|
+
};
|
|
939
|
+
const progressFor = (state, now, settings) => {
|
|
940
|
+
const durationMs = durationFor(state.phase, settings), remainingMs = remainingMsAt(state, now), ratio = Math.min(MAX_RATIO, Math.max(MIN_RATIO, (durationMs - remainingMs) / durationMs)), remainingSeconds = toDisplaySeconds(remainingMs), totalSeconds = toDisplaySeconds(durationMs);
|
|
941
|
+
return {
|
|
942
|
+
elapsedSeconds: Math.max(NO_TIME, totalSeconds - remainingSeconds),
|
|
943
|
+
percent: Math.floor(ratio * PERCENT$1),
|
|
944
|
+
ratio,
|
|
945
|
+
remainingSeconds,
|
|
946
|
+
totalSeconds
|
|
947
|
+
};
|
|
948
|
+
};
|
|
949
|
+
//#endregion
|
|
950
|
+
//#region src/terminal/timeline.ts
|
|
951
|
+
const BLOCK = "█";
|
|
952
|
+
const EMPTY_CELL = "·";
|
|
953
|
+
const EIGHTH_BLOCKS = [
|
|
954
|
+
"",
|
|
955
|
+
"▏",
|
|
956
|
+
"▎",
|
|
957
|
+
"▍",
|
|
958
|
+
"▌",
|
|
959
|
+
"▋",
|
|
960
|
+
"▊",
|
|
961
|
+
"▉"
|
|
962
|
+
];
|
|
963
|
+
const EIGHTHS_PER_CELL = 8;
|
|
964
|
+
const INDENT$1 = " ";
|
|
965
|
+
const LABEL_GAP = " ";
|
|
966
|
+
const BAR_GAP = " ";
|
|
967
|
+
const CURRENT_LABEL = "◀ now";
|
|
968
|
+
const DONE_LABEL = "✓";
|
|
969
|
+
const FIRST_CYCLE = 1;
|
|
970
|
+
const NOT_STARTED = 0;
|
|
971
|
+
const NO_CYCLES = 0;
|
|
972
|
+
const COMPLETED = 1;
|
|
973
|
+
const ONE_CELL = 1;
|
|
974
|
+
const TIMELINE_CONTEXT = 2;
|
|
975
|
+
const TIMELINE_CELLS = 20;
|
|
976
|
+
const TIMELINE_MIN_CELLS = 2;
|
|
977
|
+
const TIMELINE_MAX_ROWS = 5;
|
|
978
|
+
const createSmoothBar = (ratio, cells, tone) => {
|
|
979
|
+
const eighths = Math.floor(ratio * cells * EIGHTHS_PER_CELL), fullCells = Math.floor(eighths / EIGHTHS_PER_CELL), partial = EIGHTH_BLOCKS[eighths % EIGHTHS_PER_CELL] ?? "", filled = `${BLOCK.repeat(fullCells)}${partial}`;
|
|
980
|
+
return [{
|
|
981
|
+
text: filled,
|
|
982
|
+
tone
|
|
983
|
+
}, { text: EMPTY_CELL.repeat(cells - cellWidth(filled)) }];
|
|
984
|
+
};
|
|
985
|
+
const timelineCellsFor = (settings) => {
|
|
986
|
+
const totalMs = settings.workDurationMs + settings.breakDurationMs, work = Math.min(18, Math.max(TIMELINE_MIN_CELLS, Math.round(TIMELINE_CELLS * settings.workDurationMs / totalMs)));
|
|
987
|
+
return {
|
|
988
|
+
break: TIMELINE_CELLS - work,
|
|
989
|
+
work
|
|
990
|
+
};
|
|
991
|
+
};
|
|
992
|
+
const timelineRangeFor = (cycle, loopCount) => {
|
|
993
|
+
const lastStart = Math.max(FIRST_CYCLE, loopCount - TIMELINE_MAX_ROWS + FIRST_CYCLE), first = Math.min(lastStart, Math.max(FIRST_CYCLE, cycle - TIMELINE_CONTEXT));
|
|
994
|
+
return [first, Math.min(loopCount, first + TIMELINE_MAX_ROWS - FIRST_CYCLE)];
|
|
995
|
+
};
|
|
996
|
+
const cycleStatusFor = (row, state, ratio) => {
|
|
997
|
+
if (row < state.cycle) return {
|
|
998
|
+
breakRatio: COMPLETED,
|
|
999
|
+
label: DONE_LABEL,
|
|
1000
|
+
workRatio: COMPLETED
|
|
1001
|
+
};
|
|
1002
|
+
if (row > state.cycle) return {
|
|
1003
|
+
breakRatio: NOT_STARTED,
|
|
1004
|
+
label: "",
|
|
1005
|
+
workRatio: NOT_STARTED
|
|
1006
|
+
};
|
|
1007
|
+
if (state.phase === "work") return {
|
|
1008
|
+
breakRatio: NOT_STARTED,
|
|
1009
|
+
label: CURRENT_LABEL,
|
|
1010
|
+
workRatio: ratio
|
|
1011
|
+
};
|
|
1012
|
+
return {
|
|
1013
|
+
breakRatio: ratio,
|
|
1014
|
+
label: CURRENT_LABEL,
|
|
1015
|
+
workRatio: COMPLETED
|
|
1016
|
+
};
|
|
1017
|
+
};
|
|
1018
|
+
const createCycleRow = (label, status, cells) => {
|
|
1019
|
+
const row = [
|
|
1020
|
+
{ text: `${INDENT$1}${label}${LABEL_GAP}` },
|
|
1021
|
+
...createSmoothBar(status.workRatio, cells.work, "work"),
|
|
1022
|
+
{ text: BAR_GAP },
|
|
1023
|
+
...createSmoothBar(status.breakRatio, cells.break, "break")
|
|
1024
|
+
];
|
|
1025
|
+
if (status.label !== "") row.push({ text: `${LABEL_GAP}${status.label}` });
|
|
1026
|
+
return row;
|
|
1027
|
+
};
|
|
1028
|
+
const createLegend = (settings, cells) => {
|
|
1029
|
+
const workLegend = `work ${formatDuration(settings.workDurationMs)}`, breakLegend = `break ${formatDuration(settings.breakDurationMs)}`, breakColumn = cells.work + ONE_CELL;
|
|
1030
|
+
if (cellWidth(workLegend) < breakColumn) return `${workLegend.padEnd(breakColumn)}${breakLegend}`;
|
|
1031
|
+
return `${workLegend} · ${breakLegend}`;
|
|
1032
|
+
};
|
|
1033
|
+
const hiddenCyclesLines = (text, count) => {
|
|
1034
|
+
if (count <= NO_CYCLES) return [];
|
|
1035
|
+
return [[{ text }]];
|
|
1036
|
+
};
|
|
1037
|
+
const createTimeline = (state, settings, ratio) => {
|
|
1038
|
+
const cells = timelineCellsFor(settings), [first, last] = timelineRangeFor(state.cycle, settings.loopCount), labelWidth = cellWidth(`#${last}`), prefix = " ".repeat(cellWidth(`${INDENT$1}${LABEL_GAP}`) + labelWidth), hiddenBefore = first - FIRST_CYCLE, hiddenAfter = settings.loopCount - last, rows = Array.from({ length: last - first + FIRST_CYCLE }, (_value, index) => {
|
|
1039
|
+
const cycle = first + index;
|
|
1040
|
+
return createCycleRow(`#${cycle}`.padEnd(labelWidth), cycleStatusFor(cycle, state, ratio), cells);
|
|
1041
|
+
});
|
|
1042
|
+
return [
|
|
1043
|
+
...hiddenCyclesLines(`${prefix}⋮ ${hiddenBefore} done`, hiddenBefore),
|
|
1044
|
+
...rows,
|
|
1045
|
+
...hiddenCyclesLines(`${prefix}⋮ ${hiddenAfter} more`, hiddenAfter),
|
|
1046
|
+
[{ text: `${prefix}${createLegend(settings, cells)}` }]
|
|
1047
|
+
];
|
|
1048
|
+
};
|
|
1049
|
+
//#endregion
|
|
1050
|
+
//#region src/terminal/timer-view.ts
|
|
1051
|
+
const BAR_WIDTH = 40;
|
|
1052
|
+
const INDENT = " ";
|
|
1053
|
+
const MIN_GAP = 2;
|
|
1054
|
+
const HALF = 2;
|
|
1055
|
+
const NO_CELLS = 0;
|
|
1056
|
+
const MIN_WIDTH = 1;
|
|
1057
|
+
const PERCENT = 100;
|
|
1058
|
+
const FALLBACK_COLUMNS = 80;
|
|
1059
|
+
const RIGHT_MARGIN = 1;
|
|
1060
|
+
const COMPACT_MIN_WIDTH = 20;
|
|
1061
|
+
const BIG_DIGIT_ROWS = 5;
|
|
1062
|
+
const BIG_DIGITS = {
|
|
1063
|
+
"0": [
|
|
1064
|
+
"###",
|
|
1065
|
+
"# #",
|
|
1066
|
+
"# #",
|
|
1067
|
+
"# #",
|
|
1068
|
+
"###"
|
|
1069
|
+
],
|
|
1070
|
+
"1": [
|
|
1071
|
+
"## ",
|
|
1072
|
+
" # ",
|
|
1073
|
+
" # ",
|
|
1074
|
+
" # ",
|
|
1075
|
+
"###"
|
|
1076
|
+
],
|
|
1077
|
+
"2": [
|
|
1078
|
+
"###",
|
|
1079
|
+
" #",
|
|
1080
|
+
"###",
|
|
1081
|
+
"# ",
|
|
1082
|
+
"###"
|
|
1083
|
+
],
|
|
1084
|
+
"3": [
|
|
1085
|
+
"###",
|
|
1086
|
+
" #",
|
|
1087
|
+
"###",
|
|
1088
|
+
" #",
|
|
1089
|
+
"###"
|
|
1090
|
+
],
|
|
1091
|
+
"4": [
|
|
1092
|
+
"# #",
|
|
1093
|
+
"# #",
|
|
1094
|
+
"###",
|
|
1095
|
+
" #",
|
|
1096
|
+
" #"
|
|
1097
|
+
],
|
|
1098
|
+
"5": [
|
|
1099
|
+
"###",
|
|
1100
|
+
"# ",
|
|
1101
|
+
"###",
|
|
1102
|
+
" #",
|
|
1103
|
+
"###"
|
|
1104
|
+
],
|
|
1105
|
+
"6": [
|
|
1106
|
+
"###",
|
|
1107
|
+
"# ",
|
|
1108
|
+
"###",
|
|
1109
|
+
"# #",
|
|
1110
|
+
"###"
|
|
1111
|
+
],
|
|
1112
|
+
"7": [
|
|
1113
|
+
"###",
|
|
1114
|
+
" #",
|
|
1115
|
+
" #",
|
|
1116
|
+
" #",
|
|
1117
|
+
" #"
|
|
1118
|
+
],
|
|
1119
|
+
"8": [
|
|
1120
|
+
"###",
|
|
1121
|
+
"# #",
|
|
1122
|
+
"###",
|
|
1123
|
+
"# #",
|
|
1124
|
+
"###"
|
|
1125
|
+
],
|
|
1126
|
+
"9": [
|
|
1127
|
+
"###",
|
|
1128
|
+
"# #",
|
|
1129
|
+
"###",
|
|
1130
|
+
" #",
|
|
1131
|
+
"###"
|
|
1132
|
+
],
|
|
1133
|
+
":": [
|
|
1134
|
+
" ",
|
|
1135
|
+
"#",
|
|
1136
|
+
" ",
|
|
1137
|
+
"#",
|
|
1138
|
+
" "
|
|
1139
|
+
]
|
|
1140
|
+
};
|
|
1141
|
+
const BIG_PIXEL_ON = "██";
|
|
1142
|
+
const BIG_PIXEL_OFF = " ";
|
|
1143
|
+
const BIG_DIGIT_GAP = " ";
|
|
1144
|
+
const LINE_FILLED = "━";
|
|
1145
|
+
const LINE_EMPTY = "─";
|
|
1146
|
+
const PHASE_ICONS$1 = {
|
|
1147
|
+
break: "☕",
|
|
1148
|
+
work: "🍅"
|
|
1149
|
+
};
|
|
1150
|
+
const PHASE_LABELS = {
|
|
1151
|
+
break: "BREAK",
|
|
1152
|
+
work: "WORK"
|
|
1153
|
+
};
|
|
1154
|
+
const PAUSED_LABEL = "PAUSED";
|
|
1155
|
+
const KEY_GUIDES = {
|
|
1156
|
+
paused: "[space] resume [s] skip [q] quit",
|
|
1157
|
+
running: "[space] pause [s] skip [q] quit"
|
|
1158
|
+
};
|
|
1159
|
+
const COMPACT_KEY_GUIDES = {
|
|
1160
|
+
paused: "[sp]resume [s]skip [q]quit",
|
|
1161
|
+
running: "[sp]pause [s]skip [q]quit"
|
|
1162
|
+
};
|
|
1163
|
+
const keyGuideKind = (state) => {
|
|
1164
|
+
if (state.paused) return "paused";
|
|
1165
|
+
return "running";
|
|
1166
|
+
};
|
|
1167
|
+
const spaces = (cells) => " ".repeat(Math.max(NO_CELLS, cells));
|
|
1168
|
+
const bigPixel = (pixel) => {
|
|
1169
|
+
if (pixel === "#") return BIG_PIXEL_ON;
|
|
1170
|
+
return BIG_PIXEL_OFF;
|
|
1171
|
+
};
|
|
1172
|
+
const renderBigClock = (clock) => Array.from({ length: BIG_DIGIT_ROWS }, (_value, row) => [...clock].map((character) => [...(BIG_DIGITS[character] ?? []).at(row) ?? ""].map((pixel) => bigPixel(pixel)).join("")).join(BIG_DIGIT_GAP));
|
|
1173
|
+
const createBigClockLines = (clock, phase) => {
|
|
1174
|
+
const rows = renderBigClock(clock), offset = Math.floor((BAR_WIDTH - cellWidth(rows.at(NO_CELLS) ?? "")) / HALF);
|
|
1175
|
+
return rows.map((row) => [{ text: `${INDENT}${spaces(offset)}` }, {
|
|
1176
|
+
text: row.trimEnd(),
|
|
1177
|
+
tone: phase
|
|
1178
|
+
}]);
|
|
1179
|
+
};
|
|
1180
|
+
const createLineBar = (percent, cells, tone) => {
|
|
1181
|
+
const filled = Math.floor(percent * cells / PERCENT);
|
|
1182
|
+
return [{
|
|
1183
|
+
text: LINE_FILLED.repeat(filled),
|
|
1184
|
+
tone
|
|
1185
|
+
}, { text: LINE_EMPTY.repeat(cells - filled) }];
|
|
1186
|
+
};
|
|
1187
|
+
const spreadLabels = ([left, center, right]) => {
|
|
1188
|
+
const centerStart = Math.floor((BAR_WIDTH - cellWidth(center)) / HALF), leftGap = centerStart - cellWidth(left), rightGap = BAR_WIDTH - centerStart - cellWidth(center) - cellWidth(right);
|
|
1189
|
+
if (leftGap < MIN_GAP || rightGap < MIN_GAP) return [
|
|
1190
|
+
left,
|
|
1191
|
+
center,
|
|
1192
|
+
right
|
|
1193
|
+
].join(spaces(MIN_GAP));
|
|
1194
|
+
return `${left}${spaces(leftGap)}${center}${spaces(rightGap)}${right}`;
|
|
1195
|
+
};
|
|
1196
|
+
const phaseSegment = ({ paused, phase }) => {
|
|
1197
|
+
const label = `${PHASE_ICONS$1[phase]} ${PHASE_LABELS[phase]}`;
|
|
1198
|
+
if (paused) return {
|
|
1199
|
+
text: `${label} ${PAUSED_LABEL}`,
|
|
1200
|
+
tone: phase
|
|
1201
|
+
};
|
|
1202
|
+
return {
|
|
1203
|
+
text: label,
|
|
1204
|
+
tone: phase
|
|
1205
|
+
};
|
|
1206
|
+
};
|
|
1207
|
+
const minimalPhaseLabel = ({ paused, phase }) => {
|
|
1208
|
+
if (paused) return PAUSED_LABEL;
|
|
1209
|
+
return PHASE_LABELS[phase];
|
|
1210
|
+
};
|
|
1211
|
+
const createHeader = (state, settings) => {
|
|
1212
|
+
const phase = phaseSegment(state), cycle = `Cycle ${state.cycle} / ${settings.loopCount}`, gap = Math.max(MIN_GAP, BAR_WIDTH - cellWidth(phase.text) - cellWidth(cycle));
|
|
1213
|
+
return [
|
|
1214
|
+
{ text: INDENT },
|
|
1215
|
+
phase,
|
|
1216
|
+
{ text: `${spaces(gap)}${cycle}` }
|
|
1217
|
+
];
|
|
1218
|
+
};
|
|
1219
|
+
const createFullFrame = ({ progress, settings, state }) => [
|
|
1220
|
+
createHeader(state, settings),
|
|
1221
|
+
[],
|
|
1222
|
+
...createBigClockLines(formatClock(progress.remainingSeconds), state.phase),
|
|
1223
|
+
[],
|
|
1224
|
+
[{ text: INDENT }, ...createLineBar(progress.percent, BAR_WIDTH, state.phase)],
|
|
1225
|
+
[{ text: `${INDENT}${spreadLabels([
|
|
1226
|
+
`${formatClock(progress.elapsedSeconds)} elapsed`,
|
|
1227
|
+
`${progress.percent}%`,
|
|
1228
|
+
`${formatClock(progress.totalSeconds)} total`
|
|
1229
|
+
])}` }],
|
|
1230
|
+
[],
|
|
1231
|
+
...createTimeline(state, settings, progress.ratio),
|
|
1232
|
+
[],
|
|
1233
|
+
[{ text: `${INDENT}${KEY_GUIDES[keyGuideKind(state)]}` }]
|
|
1234
|
+
];
|
|
1235
|
+
const createCompactFrame = ({ progress, settings, state, width }) => [
|
|
1236
|
+
[phaseSegment(state), { text: `${spaces(MIN_GAP)}${state.cycle}/${settings.loopCount}` }],
|
|
1237
|
+
[{ text: `${formatClock(progress.remainingSeconds)} left · ${progress.percent}%` }],
|
|
1238
|
+
createLineBar(progress.percent, Math.min(BAR_WIDTH, width), state.phase),
|
|
1239
|
+
[{ text: COMPACT_KEY_GUIDES[keyGuideKind(state)] }]
|
|
1240
|
+
];
|
|
1241
|
+
const createMinimalFrame = ({ progress, settings, state }) => [[{
|
|
1242
|
+
text: minimalPhaseLabel(state),
|
|
1243
|
+
tone: state.phase
|
|
1244
|
+
}, { text: ` ${formatClock(progress.remainingSeconds)} ${state.cycle}/${settings.loopCount}` }]];
|
|
1245
|
+
const createTimerFrame = ({ state, now, settings, columns }) => {
|
|
1246
|
+
const width = Math.max(MIN_WIDTH, (columns || FALLBACK_COLUMNS) - RIGHT_MARGIN), layout = {
|
|
1247
|
+
progress: progressFor(state, now, settings),
|
|
1248
|
+
settings,
|
|
1249
|
+
state,
|
|
1250
|
+
width
|
|
1251
|
+
}, full = createFullFrame(layout);
|
|
1252
|
+
if (full.every((line) => lineWidth(line) <= width)) return [...full];
|
|
1253
|
+
if (width >= COMPACT_MIN_WIDTH) return createCompactFrame(layout).map((line) => truncateLine(line, width));
|
|
1254
|
+
return createMinimalFrame(layout).map((line) => truncateLine(line, width));
|
|
1255
|
+
};
|
|
1256
|
+
//#endregion
|
|
1257
|
+
//#region src/terminal/timer-keys.ts
|
|
1258
|
+
/** Ctrlなしで受け付けるキー名と操作の対応です。Shift(大文字)は区別しません。 */
|
|
1259
|
+
const PLAIN_KEYS = /* @__PURE__ */ new Map([
|
|
1260
|
+
["q", "quit"],
|
|
1261
|
+
["s", "skip"],
|
|
1262
|
+
["space", "toggle-pause"]
|
|
1263
|
+
]);
|
|
1264
|
+
const timerKeyFrom = (value) => {
|
|
1265
|
+
const key = keyFrom(value);
|
|
1266
|
+
if (typeof value === "object" && value !== null && Reflect.get(value, "meta") === true) return;
|
|
1267
|
+
if (key.ctrl) {
|
|
1268
|
+
if (key.name === "c") return "interrupt";
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
return PLAIN_KEYS.get(key.name);
|
|
1272
|
+
};
|
|
1273
|
+
const watchTimerKeys = ({ input = process$1.stdin, onKey }) => {
|
|
1274
|
+
emitKeypressEvents(input);
|
|
1275
|
+
const inputWasFlowing = input.readableFlowing === true, rawModeWasEnabled = input.isRaw === true, supportsRawMode = input.isTTY === true && typeof input.setRawMode === "function", handleKeypress = (_sequence, value) => {
|
|
1276
|
+
const key = timerKeyFrom(value);
|
|
1277
|
+
if (key !== void 0) onKey(key);
|
|
1278
|
+
};
|
|
1279
|
+
let stopped = false;
|
|
1280
|
+
input.on("keypress", handleKeypress);
|
|
1281
|
+
if (supportsRawMode && !rawModeWasEnabled) input.setRawMode?.(true);
|
|
1282
|
+
if (!inputWasFlowing) input.resume();
|
|
1283
|
+
return () => {
|
|
1284
|
+
if (stopped) return;
|
|
1285
|
+
stopped = true;
|
|
1286
|
+
input.removeListener("keypress", handleKeypress);
|
|
1287
|
+
if (supportsRawMode && !rawModeWasEnabled) input.setRawMode?.(false);
|
|
1288
|
+
if (!inputWasFlowing) input.pause();
|
|
1289
|
+
};
|
|
1290
|
+
};
|
|
1291
|
+
//#endregion
|
|
1292
|
+
//#region src/cli/progress-view.ts
|
|
1293
|
+
const PHASE_ICONS = {
|
|
1294
|
+
break: "☕",
|
|
1295
|
+
work: "🍅"
|
|
1296
|
+
};
|
|
1297
|
+
const PHASE_NAMES = {
|
|
1298
|
+
break: "Break",
|
|
1299
|
+
work: "Work"
|
|
1300
|
+
};
|
|
1301
|
+
const createPhaseStartedOutput = (phase, durationMs) => {
|
|
1302
|
+
const durationMinutes = durationMs / MILLISECONDS_PER_MINUTE;
|
|
1303
|
+
return `${PHASE_ICONS[phase]} ${PHASE_NAMES[phase]} started (${durationMinutes} min)\n`;
|
|
1304
|
+
};
|
|
1305
|
+
const createPhaseCompletedOutput = (phase) => `✅ ${PHASE_NAMES[phase]} complete.\n`;
|
|
1306
|
+
const createPhaseSkippedOutput = (phase) => `⏩ ${PHASE_NAMES[phase]} skipped.\n`;
|
|
1307
|
+
const applyTimerKey = (key, controls, onStop) => {
|
|
1308
|
+
if (key === "toggle-pause") {
|
|
1309
|
+
controls.togglePause();
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (key === "skip") {
|
|
1313
|
+
controls.skip();
|
|
1314
|
+
return;
|
|
1315
|
+
}
|
|
1316
|
+
if (!controls.settle()) onStop(key);
|
|
1317
|
+
};
|
|
1318
|
+
const isInteractiveTerminal = (stdin, stdout) => stdin.isTTY === true && stdout.isTTY === true && process$1.env.TERM !== "dumb";
|
|
1319
|
+
const createLineView = (write) => ({
|
|
1320
|
+
finish: () => {},
|
|
1321
|
+
onPhaseCompleted: (phase) => {
|
|
1322
|
+
write(createPhaseCompletedOutput(phase));
|
|
1323
|
+
},
|
|
1324
|
+
onPhaseStarted: (phase, durationMs) => {
|
|
1325
|
+
write(createPhaseStartedOutput(phase, durationMs));
|
|
1326
|
+
}
|
|
1327
|
+
});
|
|
1328
|
+
const createInteractiveView = ({ color, onStop, processEvents, settings, stdin, terminal, write }) => {
|
|
1329
|
+
const renderer = createInlineRenderer(write), guard = createTerminalGuard({
|
|
1330
|
+
onSignal: onStop,
|
|
1331
|
+
processEvents,
|
|
1332
|
+
terminal,
|
|
1333
|
+
write
|
|
1334
|
+
});
|
|
1335
|
+
let controls, latest;
|
|
1336
|
+
const draw = () => {
|
|
1337
|
+
if (latest === void 0) return;
|
|
1338
|
+
guard.hideCursor();
|
|
1339
|
+
renderer.render(createTimerFrame({
|
|
1340
|
+
columns: terminal.columns,
|
|
1341
|
+
now: latest.now,
|
|
1342
|
+
settings,
|
|
1343
|
+
state: latest.state
|
|
1344
|
+
}).map((line) => stringifyFrameLine(line, color)));
|
|
1345
|
+
}, handleResize = () => {
|
|
1346
|
+
renderer.release();
|
|
1347
|
+
draw();
|
|
1348
|
+
}, stopWatchingKeys = watchTimerKeys({
|
|
1349
|
+
input: stdin,
|
|
1350
|
+
onKey: (key) => {
|
|
1351
|
+
if (controls !== void 0) applyTimerKey(key, controls, onStop);
|
|
1352
|
+
}
|
|
1353
|
+
});
|
|
1354
|
+
terminal.on("resize", handleResize);
|
|
1355
|
+
return {
|
|
1356
|
+
bindControls: (bound) => {
|
|
1357
|
+
controls = bound;
|
|
1358
|
+
},
|
|
1359
|
+
finish: () => {
|
|
1360
|
+
stopWatchingKeys();
|
|
1361
|
+
terminal.off?.("resize", handleResize);
|
|
1362
|
+
guard.release();
|
|
1363
|
+
},
|
|
1364
|
+
onPhaseCompleted: (phase) => {
|
|
1365
|
+
renderer.log(createPhaseCompletedOutput(phase));
|
|
1366
|
+
},
|
|
1367
|
+
onPhaseSkipped: (phase) => {
|
|
1368
|
+
renderer.log(createPhaseSkippedOutput(phase));
|
|
1369
|
+
},
|
|
1370
|
+
onPhaseStarted: () => {},
|
|
1371
|
+
onTick: (state, now) => {
|
|
1372
|
+
latest = {
|
|
1373
|
+
now,
|
|
1374
|
+
state
|
|
1375
|
+
};
|
|
1376
|
+
draw();
|
|
1377
|
+
}
|
|
1378
|
+
};
|
|
1379
|
+
};
|
|
1380
|
+
const createProgressView = ({ color = isColorEnabled(process$1.env), interactive, onStop, processEvents = process$1, settings, stdin = process$1.stdin, stdout = process$1.stdout, write }) => {
|
|
1381
|
+
if (!(interactive ?? isInteractiveTerminal(stdin, stdout))) return createLineView(write);
|
|
1382
|
+
return createInteractiveView({
|
|
1383
|
+
color,
|
|
1384
|
+
onStop,
|
|
1385
|
+
processEvents,
|
|
1386
|
+
settings,
|
|
1387
|
+
stdin,
|
|
1388
|
+
terminal: stdout,
|
|
1389
|
+
write
|
|
1390
|
+
});
|
|
1391
|
+
};
|
|
1392
|
+
//#endregion
|
|
1393
|
+
//#region src/cli/parse-arguments.ts
|
|
1394
|
+
const ARGUMENT_INDEX_INCREMENT = 1;
|
|
1395
|
+
const ARGUMENTS_START_INDEX = 0;
|
|
1396
|
+
const DURATION_PATTERN = /^[1-9][0-9]{0,3}$/u;
|
|
1397
|
+
const EQUALS_SIGN = "=";
|
|
1398
|
+
const EQUALS_SIGN_NOT_FOUND = -1;
|
|
1399
|
+
const HELP_OPTIONS = ["--help", "-h"];
|
|
1400
|
+
const MAXIMUM_DURATION_MINUTES = 1440;
|
|
1401
|
+
const NEXT_ARGUMENT_OFFSET = 1;
|
|
1402
|
+
const LOOP_PATTERN = /^[1-9][0-9]*$/u;
|
|
1403
|
+
const isNumericOption = (value) => value === "--break" || value === "--work" || value === "--loop";
|
|
1404
|
+
const isHelpOption = (value) => HELP_OPTIONS.some((option) => option === value);
|
|
1405
|
+
const createValueError = (option) => {
|
|
1406
|
+
let maximum = MAXIMUM_DURATION_MINUTES;
|
|
1407
|
+
if (option === "--loop") maximum = Number.MAX_SAFE_INTEGER;
|
|
1408
|
+
return {
|
|
1409
|
+
kind: "error",
|
|
1410
|
+
message: `Error: ${option} requires an integer from 1 to ${maximum}.\n`
|
|
1411
|
+
};
|
|
1412
|
+
};
|
|
1413
|
+
const createDuplicateError = (option) => ({
|
|
1414
|
+
kind: "error",
|
|
1415
|
+
message: `Error: ${option} may only be specified once.\n`
|
|
1416
|
+
});
|
|
1417
|
+
const parseNumericValue = (option, value) => {
|
|
1418
|
+
let pattern = DURATION_PATTERN, maximum = MAXIMUM_DURATION_MINUTES;
|
|
1419
|
+
if (option === "--loop") {
|
|
1420
|
+
pattern = LOOP_PATTERN;
|
|
1421
|
+
maximum = Number.MAX_SAFE_INTEGER;
|
|
1422
|
+
}
|
|
1423
|
+
if (typeof value !== "string" || !pattern.test(value)) return;
|
|
1424
|
+
const numericValue = Number(value);
|
|
1425
|
+
if (!Number.isSafeInteger(numericValue) || numericValue > maximum) return;
|
|
1426
|
+
return numericValue;
|
|
1427
|
+
};
|
|
1428
|
+
const parseNumericArgument = (argument, nextArgument) => {
|
|
1429
|
+
const equalsSignIndex = argument.indexOf(EQUALS_SIGN);
|
|
1430
|
+
if (equalsSignIndex === EQUALS_SIGN_NOT_FOUND) return {
|
|
1431
|
+
consumesNextArgument: true,
|
|
1432
|
+
optionName: argument,
|
|
1433
|
+
value: nextArgument
|
|
1434
|
+
};
|
|
1435
|
+
return {
|
|
1436
|
+
consumesNextArgument: false,
|
|
1437
|
+
optionName: argument.slice(ARGUMENTS_START_INDEX, equalsSignIndex),
|
|
1438
|
+
value: argument.slice(equalsSignIndex + NEXT_ARGUMENT_OFFSET)
|
|
1439
|
+
};
|
|
1440
|
+
};
|
|
1441
|
+
const parseCliArguments = (arguments_) => {
|
|
1442
|
+
if (arguments_.some((argument) => isHelpOption(argument))) return { kind: "help" };
|
|
1443
|
+
const specifiedOptions = /* @__PURE__ */ new Set();
|
|
1444
|
+
let breakDurationMinutes = 5, loopCount = 3, workDurationMinutes = 15;
|
|
1445
|
+
for (let argumentIndex = ARGUMENTS_START_INDEX; argumentIndex < arguments_.length; argumentIndex += ARGUMENT_INDEX_INCREMENT) {
|
|
1446
|
+
const argument = arguments_[argumentIndex] ?? "", { consumesNextArgument, optionName, value } = parseNumericArgument(argument, arguments_[argumentIndex + NEXT_ARGUMENT_OFFSET]);
|
|
1447
|
+
if (!isNumericOption(optionName)) return {
|
|
1448
|
+
kind: "error",
|
|
1449
|
+
message: `Error: Unknown option: ${escapeDiagnostic(argument)}\n`
|
|
1450
|
+
};
|
|
1451
|
+
if (specifiedOptions.has(optionName)) return createDuplicateError(optionName);
|
|
1452
|
+
const numericValue = parseNumericValue(optionName, value);
|
|
1453
|
+
if (typeof numericValue !== "number") return createValueError(optionName);
|
|
1454
|
+
specifiedOptions.add(optionName);
|
|
1455
|
+
if (consumesNextArgument) argumentIndex += NEXT_ARGUMENT_OFFSET;
|
|
1456
|
+
if (optionName === "--work") workDurationMinutes = numericValue;
|
|
1457
|
+
else if (optionName === "--loop") loopCount = numericValue;
|
|
1458
|
+
else breakDurationMinutes = numericValue;
|
|
1459
|
+
}
|
|
1460
|
+
return {
|
|
1461
|
+
breakDurationMinutes,
|
|
1462
|
+
kind: "run",
|
|
1463
|
+
loopCount,
|
|
1464
|
+
workDurationMinutes
|
|
1465
|
+
};
|
|
1466
|
+
};
|
|
1467
|
+
//#endregion
|
|
1468
|
+
//#region src/cli/run.ts
|
|
1469
|
+
const CLI_ARGUMENTS_START_INDEX = 2;
|
|
1470
|
+
const FAILURE_EXIT_CODE$1 = 1;
|
|
1471
|
+
const SUCCESS_EXIT_CODE = 0;
|
|
1472
|
+
const STOP_EXIT_CODES = {
|
|
1473
|
+
SIGHUP: 129,
|
|
1474
|
+
SIGINT: 130,
|
|
1475
|
+
SIGTERM: 143,
|
|
1476
|
+
interrupt: 130,
|
|
1477
|
+
quit: SUCCESS_EXIT_CODE
|
|
1478
|
+
};
|
|
1479
|
+
const USAGE_ERROR_EXIT_CODE = 2;
|
|
1480
|
+
const SHUTDOWN_DEADLINE_MS = 2e3;
|
|
1481
|
+
const COMPLETION_SHUTDOWN_DEADLINE_MS = 5e3;
|
|
1482
|
+
const exitCodeForOutputFailure = (error) => {
|
|
1483
|
+
if (isBrokenPipe(error)) return SUCCESS_EXIT_CODE;
|
|
1484
|
+
return FAILURE_EXIT_CODE$1;
|
|
1485
|
+
};
|
|
1486
|
+
const emitDiagnostic = (writeError, message) => {
|
|
1487
|
+
try {
|
|
1488
|
+
writeError(message);
|
|
1489
|
+
} catch {}
|
|
1490
|
+
};
|
|
1491
|
+
const scheduleProcessExit = (exitCode, delayMs) => {
|
|
1492
|
+
setTimeout(() => {
|
|
1493
|
+
process$1.exit(exitCode);
|
|
1494
|
+
}, delayMs).unref();
|
|
1495
|
+
};
|
|
1496
|
+
const setProcessExitCode = (exitCode) => {
|
|
1497
|
+
process$1.exitCode = exitCode;
|
|
1498
|
+
};
|
|
1499
|
+
const runHelp = (stdout, setExitCode) => {
|
|
1500
|
+
let exitCode = SUCCESS_EXIT_CODE;
|
|
1501
|
+
createStandardOutput((error) => {
|
|
1502
|
+
exitCode = exitCodeForOutputFailure(error);
|
|
1503
|
+
setExitCode(exitCode);
|
|
1504
|
+
}, stdout)(createCliOutput({
|
|
1505
|
+
optionName: "--help",
|
|
1506
|
+
options: OPTIONS
|
|
1507
|
+
}));
|
|
1508
|
+
return exitCode;
|
|
1509
|
+
};
|
|
1510
|
+
const runCli = async ({ arguments_ = process$1.argv.slice(CLI_ARGUMENTS_START_INDEX), confirmStart = confirmWorkStart, createSoundPlayer, now, scheduleExit = scheduleProcessExit, setExitCode = setProcessExitCode, sleep, stdin, stdout, interactive, processEvents, color, writeError = createDiagnosticWriter() }) => {
|
|
1511
|
+
const parsedArguments = parseCliArguments(arguments_);
|
|
1512
|
+
if (parsedArguments.kind === "error") {
|
|
1513
|
+
emitDiagnostic(writeError, parsedArguments.message);
|
|
1514
|
+
return USAGE_ERROR_EXIT_CODE;
|
|
1515
|
+
}
|
|
1516
|
+
if (parsedArguments.kind === "help") return runHelp(stdout, setExitCode);
|
|
1517
|
+
const controller = new AbortController();
|
|
1518
|
+
let shutdownExitCode, soundPlayer, view;
|
|
1519
|
+
/**
|
|
1520
|
+
* 終了処理を1回だけ開始します。
|
|
1521
|
+
*
|
|
1522
|
+
* 最初の呼び出しで終了コードを固定し、確認待ちとタイマーを中断して、
|
|
1523
|
+
* 再生中の子プロセスへ終了要求を送ります。親プロセスは期限に達すると
|
|
1524
|
+
* 強制終了するため、終了しない子プロセスがあっても残り続けません。
|
|
1525
|
+
*/
|
|
1526
|
+
const requestShutdown = (exitCode, complete) => {
|
|
1527
|
+
if (shutdownExitCode !== void 0) return shutdownExitCode;
|
|
1528
|
+
shutdownExitCode = exitCode;
|
|
1529
|
+
setExitCode(exitCode);
|
|
1530
|
+
controller.abort();
|
|
1531
|
+
soundPlayer?.shutdown({ complete });
|
|
1532
|
+
if (complete) scheduleExit(exitCode, COMPLETION_SHUTDOWN_DEADLINE_MS);
|
|
1533
|
+
else scheduleExit(exitCode, SHUTDOWN_DEADLINE_MS);
|
|
1534
|
+
return exitCode;
|
|
1535
|
+
}, writeOutput = createStandardOutput((error) => {
|
|
1536
|
+
requestShutdown(exitCodeForOutputFailure(error), false);
|
|
1537
|
+
}, stdout);
|
|
1538
|
+
try {
|
|
1539
|
+
soundPlayer = createSoundPlayer();
|
|
1540
|
+
writeOutput(createCliOutput({
|
|
1541
|
+
columns: (stdout ?? process$1.stdout).columns,
|
|
1542
|
+
optionName: "",
|
|
1543
|
+
options: OPTIONS
|
|
1544
|
+
}));
|
|
1545
|
+
if (shutdownExitCode !== void 0) return shutdownExitCode;
|
|
1546
|
+
const confirmed = await confirmStart({
|
|
1547
|
+
signal: controller.signal,
|
|
1548
|
+
write: writeOutput
|
|
1549
|
+
});
|
|
1550
|
+
if (shutdownExitCode !== void 0) return shutdownExitCode;
|
|
1551
|
+
if (!confirmed) {
|
|
1552
|
+
writeOutput("⏹️ Work was not started.\n");
|
|
1553
|
+
return requestShutdown(SUCCESS_EXIT_CODE, false);
|
|
1554
|
+
}
|
|
1555
|
+
const player = soundPlayer, settings = {
|
|
1556
|
+
breakDurationMs: parsedArguments.breakDurationMinutes * MILLISECONDS_PER_MINUTE,
|
|
1557
|
+
loopCount: parsedArguments.loopCount,
|
|
1558
|
+
workDurationMs: parsedArguments.workDurationMinutes * MILLISECONDS_PER_MINUTE
|
|
1559
|
+
};
|
|
1560
|
+
view = createProgressView({
|
|
1561
|
+
color,
|
|
1562
|
+
interactive,
|
|
1563
|
+
onStop: (reason) => {
|
|
1564
|
+
requestShutdown(STOP_EXIT_CODES[reason], false);
|
|
1565
|
+
},
|
|
1566
|
+
processEvents,
|
|
1567
|
+
settings,
|
|
1568
|
+
stdin,
|
|
1569
|
+
stdout,
|
|
1570
|
+
write: writeOutput
|
|
1571
|
+
});
|
|
1572
|
+
const timerView = view, result = await runPomodoroTimer({
|
|
1573
|
+
...settings,
|
|
1574
|
+
bindControls: timerView.bindControls,
|
|
1575
|
+
now,
|
|
1576
|
+
onPhaseCompleted: (phase, { final }) => {
|
|
1577
|
+
timerView.onPhaseCompleted(phase);
|
|
1578
|
+
player.play(phase, { completionNotice: final });
|
|
1579
|
+
},
|
|
1580
|
+
onPhaseSkipped: timerView.onPhaseSkipped,
|
|
1581
|
+
onPhaseStarted: timerView.onPhaseStarted,
|
|
1582
|
+
onTick: timerView.onTick,
|
|
1583
|
+
signal: controller.signal,
|
|
1584
|
+
sleep
|
|
1585
|
+
});
|
|
1586
|
+
view.finish();
|
|
1587
|
+
if (!result.completed) return shutdownExitCode ?? FAILURE_EXIT_CODE$1;
|
|
1588
|
+
writeOutput("🎉 Pomodoro complete.\n");
|
|
1589
|
+
return requestShutdown(SUCCESS_EXIT_CODE, true);
|
|
1590
|
+
} catch (error) {
|
|
1591
|
+
view?.finish();
|
|
1592
|
+
requestShutdown(FAILURE_EXIT_CODE$1, false);
|
|
1593
|
+
throw error;
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
//#endregion
|
|
1597
|
+
//#region src/cli.ts
|
|
1598
|
+
const FAILURE_EXIT_CODE = 1;
|
|
1599
|
+
const SOUND_DIRECTORY = new URL("../assets/", import.meta.url);
|
|
1600
|
+
const UNCONVERTIBLE_VALUE_MESSAGE = "Unable to convert the thrown value to a string";
|
|
1601
|
+
const stringifyError = (error) => {
|
|
1602
|
+
try {
|
|
1603
|
+
return String(error);
|
|
1604
|
+
} catch {
|
|
1605
|
+
return UNCONVERTIBLE_VALUE_MESSAGE;
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
const handleFailure = (error) => {
|
|
1609
|
+
process$1.exitCode = FAILURE_EXIT_CODE;
|
|
1610
|
+
createDiagnosticWriter()(`Error: ${escapeDiagnostic(stringifyError(error))}\n`);
|
|
1611
|
+
};
|
|
1612
|
+
try {
|
|
1613
|
+
process$1.exitCode = await runCli({ createSoundPlayer: () => createSoundPlayer({
|
|
1614
|
+
commands: resolvePlayerCommands(),
|
|
1615
|
+
soundDirectory: SOUND_DIRECTORY
|
|
1616
|
+
}) });
|
|
1617
|
+
} catch (error) {
|
|
1618
|
+
handleFailure(error);
|
|
1619
|
+
}
|
|
1620
|
+
//#endregion
|
|
1621
|
+
export {};
|