denoland / denoland/std

`@std/cli`: Support multiple spinners at the same time

Open
#5,037 12 comments 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.