Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): command line spinner #3968

Merged
merged 12 commits into from
Dec 20, 2023
1 change: 1 addition & 0 deletions cli/mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@

export * from "./parse_args.ts";
export * from "./prompt_secret.ts";
export * from "./spinner.ts";
109 changes: 109 additions & 0 deletions cli/spinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

const encoder = new TextEncoder();

const LINE_CLEAR = encoder.encode("\r\u001b[K"); // From cli/prompt_secret.ts
const COLOR_RESET = "\u001b[0m";
const DEFAULT_SPEED = 75;
const DEFAULT_SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];

/** Options for {@linkcode Spinner}. */
export interface SpinnerOptions {
/**
* The sequence of characters to be iterated through for animation.
*
* @default {["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]}
*/
spinner?: string[];
/** The message to display next to the spinner. */
message?: string;
/** The speed of the spinner.
*
* @default {75}
*/
speed?: number;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

speed sounds strange to me as lower number gives faster speed of spinner. How about internal? ref: https://github.com/sindresorhus/ora?tab=readme-ov-file#interval

/** The color of the spinner. Defaults to the default terminal color. */
color?: string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems this needs to be ANSI color code. On the other hand, ora supports human-readable color names ('black' | 'red' | 'green' | 'yellow' | 'blue' | 'magenta' | 'cyan' | 'white' | 'gray'). I wonder which is the better design..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it definitely makes it easier to read and write quickly. I'll make it support both 👍

}

/**
* A spinner that can be used to indicate that something is loading.
*/
export class Spinner {
#spinner: string[];
#message: string;
#speed: number;
#color?: string;
#intervalId: number | undefined;
#active = false;

/**
* Creates a new spinner.
*
* @example
* ```ts
* import { Spinner } from "https://deno.land/std@$STD_VERSION/cli/spinner.ts";
*
* const spinner = new Spinner({ message: "Loading..." });
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
* ```
*/
constructor(options?: SpinnerOptions) {
this.#spinner = options?.spinner ?? DEFAULT_SPINNER;
this.#message = options?.message ?? "";
this.#speed = options?.speed ?? DEFAULT_SPEED;
this.#color = options?.color;
}

/**
* Starts the spinner.
*
* @example
* ```ts
* import { Spinner } from "https://deno.land/std@$STD_VERSION/cli/spinner.ts";
*
* const spinner = new Spinner({ message: "Loading..." });
* spinner.start();
* ```
*/
start() {
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
if (this.#active || Deno.stdout.writable.locked) return;
this.#active = true;
let i = 0;
const color = this.#color ?? "";

// Updates the spinner after the given interval.
const updateFrame = () => {
Deno.stdout.writeSync(LINE_CLEAR);
const frame = encoder.encode(
color + this.#spinner[i] + COLOR_RESET + " " + this.#message,
);
Deno.stdout.writeSync(frame);
i = (i + 1) % this.#spinner.length;
};
this.#intervalId = setInterval(updateFrame, this.#speed);
}
/**
* Stops the spinner.
*
* @example
* ```ts
* import { Spinner } from "https://deno.land/std@$STD_VERSION/cli/spinner.ts";
*
* const spinner = new Spinner({ message: "Loading..." });
* spinner.start();
*
* setTimeout(() => {
* spinner.stop();
* console.log("Finished loading!");
* }, 3000);
* ```
*/
stop() {
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
if (this.#intervalId && this.#active) {
clearInterval(this.#intervalId);
Deno.stdout.writeSync(LINE_CLEAR); // Clear the current line
Deno.removeSignalListener("SIGINT", this.stop.bind(this));
this.#active = false;
}
}
}