`@std/cli`: Support multiple spinners at the same time
- Dominant language
- TypeScript
- Stars
- 3.6k
- Forks
- 681
- PR merge metrics
- No merged PRs in 30d
Description
Currently, if one tries to display multiple spinners at the same time, the spinners "override" each other.
```js
import { Spinner } from "@std/cli";
const spinner1 = new Spinner({
color: "yellow",
message: "Loading..."
});
const spinner2 = new Spinner({
color: "red",
message: "Loading..."
});
const spinner3 = new Spinner({
color: "blue",
message: "Loading..."
});
spinner1.start();
setTimeout(() => {
spinner1.stop();
console.info("Finished loading!");
}, 3_000);
setTimeout(() => {
spinner2.start();
setTimeout(() => {
spinner2.stop();
console.info("Finished loading!");
}, 3_000);
}, 1_000);
setTimeout(() => {
spinner3.start();
setTimeout(() => {
spinner3.stop();
console.info("Finished loading!");
}, 2_000);
}, 4_000);
```
Running the above code looks like this:
https://github.com/denoland/deno_std/assets/20396367/d5904ab4-5f57-4969-8629-1cf5dfd443f2
Therefore I propose enhancing the existing `Spinner` class or even adding a new `MultiSpinner` class to avoid introducing breaking changes, that is able to handle multiple spinners, like so:
```js
import { Spinner } from "@std/cli";
import { ansi } from "@cliffy/ansi";
const activeMultiSpinners = [];
const MultiSpinner = class extends Spinner {
static #updateMultiSpinners = () => {
const runningMultiSpinners = activeMultiSpinners
.filter((multiSpinner) => multiSpinner.running);
for (const [index, multiSpinner] of runningMultiSpinners.entries()) {
if (runningMultiSpinners.length > 1) {
multiSpinner.message = index === runningMultiSpinners.length - 1
? `${multiSpinner.actualMessage}${ansi.cursorUp(index)}`
: `${multiSpinner.actualMessage}${ansi.cursorDown(1)}`;
}
else {
multiSpinner.message = multiSpinner.actualMessage;
}
}
};
actualMessage = "";
running = false;
constructor({
color,
message
} = {}) {
super({
color,
message
});
this.actualMessage = message;
activeMultiSpinners.push(this);
}
start() {
this.running = true;
MultiSpinner.#updateMultiSpinners();
super.start();
}
stop(message) {
this.running = false;
MultiSpinner.#updateMultiSpinners();
super.stop();
}
};
export default MultiSpinner;
```
Running the example from before with this new `MultiSpinner` class looks like this:
https://github.com/denoland/deno_std/assets/20396367/2591502e-4dbe-43c1-9cc6-79c0f5ed9daa
Obviously my code was just a quick test to see if this even works, I used `@cliffy/ansi` to make it simpler, a global `activeMultiSpinners` array is probably not the way to go and timing might also be buggy (no idea in which order `updateMultiSpinners` and `stop` should be called). And I don't even want to know what happens if something like ⏎ is pressed. 😅
But yeah, what do you think about this addition? I personally was a bit surprised it was that easy to augment the base `Spinner` class, so it might not be that difficult to actually add this, using the same `cursorDown(1)` and `cursorUp(length - 1)` logic.
Contributor guide
Research direction
Start by reading the existing Spinner entry point exposed by @std/cli, then reproduce the three-spinner example from the issue to understand the current overwrite behavior. Compare the proposed cursor movement and lifecycle handling, including stopping one spinner while others run. Done means concurrent spinners render independently without breaking existing Spinner behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- cli
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100