Skip to content

Commit

Permalink
Merge pull request #27 from withastro/feat/queue
Browse files Browse the repository at this point in the history
Adds a task queue loading spinner
  • Loading branch information
natemoo-re authored Dec 18, 2023
2 parents 2c63be2 + fbec51e commit c12279b
Show file tree
Hide file tree
Showing 2 changed files with 113 additions and 7 deletions.
27 changes: 27 additions & 0 deletions .changeset/breezy-chairs-hope.md
Original file line number Diff line number Diff line change
@@ -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);
```
93 changes: 86 additions & 7 deletions src/spinner/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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);
Expand All @@ -113,8 +118,7 @@ export async function spinner(
}: { start: string; end: string; onError?: (e: any) => void; while: (...args: any) => Promise<any> },
{ 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);

Expand All @@ -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();
}
}
}

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<any>
}

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'));
}

0 comments on commit c12279b

Please sign in to comment.