From 5172f2b818ae5825652c04be9f25ac3f8db56a10 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 18 Dec 2023 14:22:26 -0600 Subject: [PATCH 1/3] feat: add task queue --- src/index.ts | 10 +++++ src/spinner/index.ts | 93 ++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 96 insertions(+), 7 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2a527c6..1196dca 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,3 +3,13 @@ export { default as prompt } from './prompt/prompt.js'; export * from './spinner/index.js'; export * from './messages/index.js'; export * from './project/index.js'; + +import { tasks } from './spinner/index.js'; +import { randomBetween, sleep } from './utils/index.js'; + +await tasks({ start: 'Initializing project...', end: 'Project initialized!' }, Array.from({ length: 5 }, (_, i) => ({ + start: `Task ${i + 1} initializing`, + end: `Task ${i + 1} completed`, + pending: `Task ${i + 1}`, + while: () => sleep(randomBetween(0, 2500)) +}))) diff --git a/src/spinner/index.ts b/src/spinner/index.ts index 356a6f1..58e12bc 100644 --- a/src/spinner/index.ts +++ b/src/spinner/index.ts @@ -67,9 +67,10 @@ async function gradient( process.exit(0); } if (stdin.isTTY) stdin.setRawMode(true); - stdout.write(cursor.hide + erase.lines(1)); + stdout.write(cursor.hide + erase.lines(text.split('\n').length)); }; + let refresh = () => {}; let done = false; const spinner = { start() { @@ -85,13 +86,17 @@ async function gradient( i = 0; } let frame = frames[i]; - logUpdate(`${frame} ${text}`); + refresh = () => logUpdate(`${frame} ${text}`); + refresh(); if (!done) await sleep(90); loop(); }; - loop(); }, + update(value: string) { + text = value; + refresh(); + }, stop() { done = true; stdin.removeListener("keypress", keypress); @@ -113,8 +118,7 @@ export async function spinner( }: { start: string; end: string; onError?: (e: any) => void; while: (...args: any) => Promise }, { stdin = process.stdin, stdout = process.stdout } = {} ) { - const loading = await gradient(chalk.green(start), { stdin, stdout }); - + const loading = await gradient(start, { stdin, stdout }); const act = update(); const tooslow = Object.create(null); @@ -123,11 +127,86 @@ export async function spinner( if (result === tooslow) { await act; } - stdout.write(`${" ".repeat(5)} ${chalk.green("✔")} ${chalk.green(end)}\n`); } catch (e) { onError?.(e); } finally { loading.stop(); } -} \ No newline at end of file +} + +const TASK_SUCCESS_FLASH = 750; +const TASK_INDENT = 5; +export interface Task { + start: string, + end: string, + pending: string; + onError?: (e: any) => void; + while: (...args: any) => Promise +} + +function formatTask(task: Task, state: 'start' | 'end' | 'pending' | 'success') { + switch (state) { + case 'start': return `${" ".repeat(TASK_INDENT + 3)} ${chalk.cyan(`▶ ${task.start}`)}`; + case 'pending': return `${" ".repeat(TASK_INDENT + 3)} ${chalk.dim(`□ ${task.pending}`)}`; + case 'success': return `${" ".repeat(TASK_INDENT + 3)} ${chalk.green(`✔ ${task.end}`)}`; + case 'end': return `${" ".repeat(TASK_INDENT + 3)} ${chalk.dim(`■ ${task.end}`)}`; + } +} +/** + * Displays a spinner while executing a list of sequential tasks + * Note that the tasks are not parallelized! A task is implicitly dependent on the tasks that preceed it. + * + * @param labels configures the start and end labels for the task queue + * @param tasks is an array of tasks that will be displayed as a list + * @param options can be used to the source of `stdin` and `stdout` + */ +export async function tasks({ start, end }: { start: string, end: string}, t: Task[], { stdin = process.stdin, stdout = process.stdout } = {}) { + let text: string[] = Array.from({ length: t.length + 1 }, () => ''); + text[0] = start; + t.forEach((task, i) => { + const state = i === 0 ? 'start' : 'pending'; + text[i + 1] = formatTask(task, state); + }) + const loading = await gradient(text.join('\n'), { stdin, stdout }); + + const refresh = () => loading.update(text.join('\n')); + + let action; + let i = 0; + let timeouts: NodeJS.Timeout[] = []; + + for (const task of t) { + i++; + text[i] = formatTask(task, 'start'); + refresh(); + action = task.while(); + try { + await action; + text[i] = formatTask(task, 'success'); + refresh(); + + const active = { i, task }; + timeouts.push( + setTimeout(() => { + const { i, task } = active; + text[i] = formatTask(task, 'end'); + refresh(); + }, TASK_SUCCESS_FLASH) + ) + } catch (e) { + loading.stop(); + task.onError?.(e); + } + } + for (const timeout of timeouts) { + clearTimeout(timeout); + } + await sleep(TASK_SUCCESS_FLASH); + loading.stop(); + text[0] = `${" ".repeat(TASK_INDENT)} ${chalk.green("✔")} ${chalk.green(end)}`; + t.forEach((task, i) => { + text[i + 1] = formatTask(task, 'end') + }) + console.log(text.join('\n')); +} From 23d37a5314e1a6edf1c5715a731399e3e51bd8e4 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 18 Dec 2023 14:25:01 -0600 Subject: [PATCH 2/3] chore: revert demo --- src/index.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1196dca..2a527c6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,3 @@ export { default as prompt } from './prompt/prompt.js'; export * from './spinner/index.js'; export * from './messages/index.js'; export * from './project/index.js'; - -import { tasks } from './spinner/index.js'; -import { randomBetween, sleep } from './utils/index.js'; - -await tasks({ start: 'Initializing project...', end: 'Project initialized!' }, Array.from({ length: 5 }, (_, i) => ({ - start: `Task ${i + 1} initializing`, - end: `Task ${i + 1} completed`, - pending: `Task ${i + 1}`, - while: () => sleep(randomBetween(0, 2500)) -}))) From fbec51ebb693b258d8c62d743ddcd924c9d96fd9 Mon Sep 17 00:00:00 2001 From: Nate Moore Date: Mon, 18 Dec 2023 14:35:48 -0600 Subject: [PATCH 3/3] chore: add changeset --- .changeset/breezy-chairs-hope.md | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 .changeset/breezy-chairs-hope.md diff --git a/.changeset/breezy-chairs-hope.md b/.changeset/breezy-chairs-hope.md new file mode 100644 index 0000000..03c4468 --- /dev/null +++ b/.changeset/breezy-chairs-hope.md @@ -0,0 +1,27 @@ +--- +"@astrojs/cli-kit": minor +--- + +Adds a new `tasks` utility that displays a spinner for multiple, sequential tasks. + +```js +import { tasks } from "@astrojs/cli-kit"; + +const queue = [ + { + pending: "Task 1", + start: "Task 1 initializing", + end: "Task 1 completed", + // async callback will be called and awaited sequentially + while: () => someAsyncAction(), + }, + // etc +]; + +const labels = { + start: "Project initializing...", + end: "Project initialized!", +}; + +await tasks(labels, queue); +```