@ayonli/jsext
Version:
A JavaScript extension package for building strong and modern applications.
134 lines (116 loc) • 3.75 kB
text/typescript
import bytes, { equals } from "../../bytes.ts";
import type { ProgressFunc, ProgressState } from "../../dialog.ts";
import {
ControlKeys,
ControlSequences,
getWindowSize,
stringWidth,
writeStdoutSync,
} from "../../cli.ts";
const { CTRL_C, ESC, LF } = ControlKeys;
const { CLR } = ControlSequences;
const ongoingIndicators = [
" ",
"= ",
"== ",
"=== ",
" === ",
" === ",
" ===",
" ==",
" =",
" ",
];
export async function handleTerminalProgress(
message: string,
fn: ProgressFunc<any>,
options: {
signal: AbortSignal;
abort?: (() => void) | undefined;
listenForAbort?: (() => Promise<any>) | undefined;
}
) {
const { signal, abort, listenForAbort } = options;
let lastMessage = message;
let lastPosition = 0;
let lastPercent: number | undefined = undefined;
const renderSimpleBar = (position: number | undefined = undefined) => {
position ??= lastPosition++;
const ongoingIndicator = ongoingIndicators[position];
writeStdoutSync(CLR);
writeStdoutSync(bytes(`${lastMessage} [${ongoingIndicator}]`));
if (lastPosition === ongoingIndicators.length) {
lastPosition = 0;
}
};
const renderPercentageBar = (percent: number) => {
const { width } = getWindowSize();
const percentage = percent + "%";
const barWidth = width - stringWidth(lastMessage) - percentage.length - 5;
const filled = "".padStart(Math.floor(barWidth * percent / 100), "#");
const empty = "".padStart(barWidth - filled.length, "-");
writeStdoutSync(CLR);
writeStdoutSync(bytes(`${lastMessage} [${filled}${empty}] ${percentage}`));
};
renderSimpleBar();
const waitingTimer = setInterval(renderSimpleBar, 200);
const set = (state: ProgressState) => {
if (signal.aborted) {
return;
}
if (state.message) {
lastMessage = state.message;
}
if (state.percent !== undefined) {
lastPercent = state.percent;
}
if (lastPercent !== undefined) {
renderPercentageBar(lastPercent);
clearInterval(waitingTimer);
} else if (state.message) {
renderSimpleBar(lastPosition);
}
};
const nodeReader = typeof Deno === "object" ? null : (buf: Uint8Array) => {
if (equals(buf, ESC) || equals(buf, CTRL_C)) {
abort?.();
}
};
const denoReader = typeof Deno === "object" ? Deno.stdin.readable.getReader() : null;
if (abort) {
if (nodeReader) {
process.stdin.on("data", nodeReader);
} else if (denoReader) {
(async () => {
while (true) {
try {
const { done, value } = await denoReader.read();
if (done || equals(value, ESC) || equals(value, CTRL_C)) {
signal.aborted || abort();
break;
}
} catch {
signal.aborted || abort();
break;
}
}
denoReader.releaseLock();
})();
}
}
let job = fn(set, signal);
if (listenForAbort) {
job = Promise.race([job, listenForAbort()]);
}
try {
return await job;
} finally {
writeStdoutSync(LF);
clearInterval(waitingTimer);
if (nodeReader) {
process.stdin.off("data", nodeReader);
} else if (denoReader) {
denoReader.releaseLock();
}
}
}