Skip to content

Commit

Permalink
feat(cli): command line spinner (#3968)
Browse files Browse the repository at this point in the history
Co-authored-by: Asher Gomez <ashersaupingomez@gmail.com>
Co-authored-by: Yoshiya Hinosawa <stibium121@gmail.com>
  • Loading branch information
3 people authored Dec 20, 2023
1 parent 1b56394 commit bc03e0d
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 0 deletions.
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";
139 changes: 139 additions & 0 deletions cli/spinner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.

// TODO(kt3k): Write test when pty is supported in Deno
// See: https://github.com/denoland/deno/issues/3994

const encoder = new TextEncoder();

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

// This is a hack to allow us to use the same type for both the color name and an ANSI escape code.
// ref: https://github.com/microsoft/TypeScript/issues/29729#issuecomment-460346421
// deno-lint-ignore ban-types
type Ansi = string & {};
type Color =
| "black"
| "red"
| "green"
| "yellow"
| "blue"
| "magenta"
| "cyan"
| "white"
| "gray"
| Ansi;

const COLORS: Record<Color, string> = {
black: "\u001b[30m",
red: "\u001b[31m",
green: "\u001b[32m",
yellow: "\u001b[33m",
blue: "\u001b[34m",
magenta: "\u001b[35m",
cyan: "\u001b[36m",
white: "\u001b[37m",
gray: "\u001b[90m",
};

/** 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 time between each frame of the spinner.
*
* @default {75}
*/
interval?: number;
/** The color of the spinner. Defaults to the default terminal color. */
color?: Color;
}

/**
* A spinner that can be used to indicate that something is loading.
*/
export class Spinner {
#spinner: string[];
#message: string;
#interval: number;
#color?: Color;
#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..." });
* ```
*/
constructor(options?: SpinnerOptions) {
this.#spinner = options?.spinner ?? DEFAULT_SPINNER;
this.#message = options?.message ?? "";
this.#interval = options?.interval ?? DEFAULT_INTERVAL;
this.#color = options?.color ? COLORS[options.color] : undefined;
}

/**
* 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() {
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.#interval);
}
/**
* 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() {
if (this.#intervalId && this.#active) {
clearInterval(this.#intervalId);
Deno.stdout.writeSync(LINE_CLEAR); // Clear the current line
this.#active = false;
}
}
}

0 comments on commit bc03e0d

Please sign in to comment.