feat: refresh CLI output styling and progress
This commit is contained in:
138
src/cli/progress.ts
Normal file
138
src/cli/progress.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { spinner } from "@clack/prompts";
|
||||
import {
|
||||
createOscProgressController,
|
||||
supportsOscProgress,
|
||||
} from "osc-progress";
|
||||
|
||||
import { theme } from "../terminal/theme.js";
|
||||
|
||||
const DEFAULT_DELAY_MS = 300;
|
||||
let activeProgress = 0;
|
||||
|
||||
type ProgressOptions = {
|
||||
label: string;
|
||||
indeterminate?: boolean;
|
||||
total?: number;
|
||||
enabled?: boolean;
|
||||
delayMs?: number;
|
||||
stream?: NodeJS.WriteStream;
|
||||
fallback?: "spinner" | "none";
|
||||
};
|
||||
|
||||
export type ProgressReporter = {
|
||||
setLabel: (label: string) => void;
|
||||
setPercent: (percent: number) => void;
|
||||
tick: (delta?: number) => void;
|
||||
done: () => void;
|
||||
};
|
||||
|
||||
const noopReporter: ProgressReporter = {
|
||||
setLabel: () => {},
|
||||
setPercent: () => {},
|
||||
tick: () => {},
|
||||
done: () => {},
|
||||
};
|
||||
|
||||
export function createCliProgress(options: ProgressOptions): ProgressReporter {
|
||||
if (options.enabled === false) return noopReporter;
|
||||
if (activeProgress > 0) return noopReporter;
|
||||
|
||||
const stream = options.stream ?? process.stderr;
|
||||
if (!stream.isTTY) return noopReporter;
|
||||
|
||||
const delayMs =
|
||||
typeof options.delayMs === "number" ? options.delayMs : DEFAULT_DELAY_MS;
|
||||
const canOsc = supportsOscProgress(process.env, stream.isTTY);
|
||||
const allowSpinner =
|
||||
!canOsc && (options.fallback === undefined || options.fallback === "spinner");
|
||||
|
||||
let started = false;
|
||||
let label = options.label;
|
||||
let total = options.total ?? null;
|
||||
let completed = 0;
|
||||
let percent = 0;
|
||||
let indeterminate =
|
||||
options.indeterminate ??
|
||||
(options.total === undefined || options.total === null);
|
||||
|
||||
activeProgress += 1;
|
||||
|
||||
const controller = canOsc
|
||||
? createOscProgressController({
|
||||
env: process.env,
|
||||
isTty: stream.isTTY,
|
||||
write: (chunk) => stream.write(chunk),
|
||||
})
|
||||
: null;
|
||||
|
||||
const spin = allowSpinner ? spinner() : null;
|
||||
let timer: NodeJS.Timeout | null = null;
|
||||
|
||||
const applyState = () => {
|
||||
if (!started) return;
|
||||
if (controller) {
|
||||
if (indeterminate) controller.setIndeterminate(label);
|
||||
else controller.setPercent(label, percent);
|
||||
} else if (spin) {
|
||||
spin.message(theme.accent(label));
|
||||
}
|
||||
};
|
||||
|
||||
const start = () => {
|
||||
if (started) return;
|
||||
started = true;
|
||||
if (spin) {
|
||||
spin.start(theme.accent(label));
|
||||
}
|
||||
applyState();
|
||||
};
|
||||
|
||||
timer = setTimeout(start, delayMs);
|
||||
|
||||
const setLabel = (next: string) => {
|
||||
label = next;
|
||||
applyState();
|
||||
};
|
||||
|
||||
const setPercent = (nextPercent: number) => {
|
||||
percent = Math.max(0, Math.min(100, Math.round(nextPercent)));
|
||||
indeterminate = false;
|
||||
applyState();
|
||||
};
|
||||
|
||||
const tick = (delta = 1) => {
|
||||
if (!total) return;
|
||||
completed = Math.min(total, completed + delta);
|
||||
const nextPercent =
|
||||
total > 0 ? Math.round((completed / total) * 100) : 0;
|
||||
setPercent(nextPercent);
|
||||
};
|
||||
|
||||
const done = () => {
|
||||
if (timer) {
|
||||
clearTimeout(timer);
|
||||
timer = null;
|
||||
}
|
||||
if (!started) {
|
||||
activeProgress = Math.max(0, activeProgress - 1);
|
||||
return;
|
||||
}
|
||||
if (controller) controller.clear();
|
||||
if (spin) spin.stop();
|
||||
activeProgress = Math.max(0, activeProgress - 1);
|
||||
};
|
||||
|
||||
return { setLabel, setPercent, tick, done };
|
||||
}
|
||||
|
||||
export async function withProgress<T>(
|
||||
options: ProgressOptions,
|
||||
work: (progress: ProgressReporter) => Promise<T>,
|
||||
): Promise<T> {
|
||||
const progress = createCliProgress(options);
|
||||
try {
|
||||
return await work(progress);
|
||||
} finally {
|
||||
progress.done();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user