From e190f9b090246d1efc8de74fa94b670c068fc8b3 Mon Sep 17 00:00:00 2001 From: Pooya Parsa Date: Sat, 28 Dec 2024 22:42:12 +0100 Subject: [PATCH] feat: animated spinner --- examples/spinner.ts | 17 +++++++++---- src/reporters/basic.ts | 7 ++++-- src/reporters/fancy.ts | 41 ++++++++++++++++++++++++++++-- src/utils/spinner.ts | 57 ++++++++++++++++++++++++++++++++++++++++++ src/utils/stream.ts | 31 ++++++++++++----------- 5 files changed, 129 insertions(+), 24 deletions(-) create mode 100644 src/utils/spinner.ts diff --git a/examples/spinner.ts b/examples/spinner.ts index d75ccc88..3ff7d70e 100755 --- a/examples/spinner.ts +++ b/examples/spinner.ts @@ -1,9 +1,16 @@ import { consola } from "./utils"; -async function main() { - consola.start("Creating project..."); - await new Promise((resolve) => setTimeout(resolve, 1000)); - consola.success("Project created!"); +consola.wrapAll(); + +// consola.start("Creating project"); +consola.start("Creating project \n Name: 123"); + +for (let i = 0; i <= 100; i++) { + if (i % 25 === 0) { + console.log(`Random info message ${i}`); + // consola.info(`Random info message ${i}`); + } + await new Promise((resolve) => setTimeout(resolve, 10)); } -main(); +consola.success("Project created!"); diff --git a/src/reporters/basic.ts b/src/reporters/basic.ts index e882beab..775ba51f 100644 --- a/src/reporters/basic.ts +++ b/src/reporters/basic.ts @@ -75,12 +75,15 @@ export class BasicReporter implements ConsolaReporter { ]); } - log(logObj: LogObject, ctx: { options: ConsolaOptions }) { - const line = this.formatLogObj(logObj, { + formatLine(logObj: LogObject, ctx: { options: ConsolaOptions }) { + return this.formatLogObj(logObj, { columns: (ctx.options.stdout as any).columns || 0, ...ctx.options.formatOptions, }); + } + log(logObj: LogObject, ctx: { options: ConsolaOptions }) { + const line = this.formatLine(logObj, ctx); return writeStream( line + "\n", logObj.level < 2 diff --git a/src/reporters/fancy.ts b/src/reporters/fancy.ts index 4ab5d43b..9d659880 100644 --- a/src/reporters/fancy.ts +++ b/src/reporters/fancy.ts @@ -2,11 +2,12 @@ import _stringWidth from "string-width"; import isUnicodeSupported from "is-unicode-supported"; import { colors } from "../utils/color"; import { parseStack } from "../utils/error"; -import { FormatOptions, LogObject } from "../types"; +import type { ConsolaOptions, FormatOptions, LogObject } from "../types"; import { LogLevel, LogType } from "../constants"; import { BoxOpts, box } from "../utils/box"; import { stripAnsi } from "../utils"; import { BasicReporter } from "./basic"; +import { Spinner } from "../utils/spinner"; export const TYPE_COLOR_MAP: { [k in LogType]?: string } = { info: "cyan", @@ -33,10 +34,12 @@ const TYPE_ICONS: { [k in LogType]?: string } = { debug: s("⚙", "D"), trace: s("→", "→"), fail: s("✖", "×"), - start: s("◐", "o"), + start: "", log: "", }; +const SPINNER_STOP_TYPES = new Set(["success", "fail", "fatal", "error"]); + function stringWidth(str: string) { // https://github.com/unjs/consola/issues/204 const hasICU = typeof Intl === "object"; @@ -47,6 +50,8 @@ function stringWidth(str: string) { } export class FancyReporter extends BasicReporter { + _spinner?: Spinner; + formatStack(stack: string, opts: FormatOptions) { const indent = " ".repeat((opts?.errorLevel || 0) + 1); return ( @@ -132,6 +137,38 @@ export class FancyReporter extends BasicReporter { return isBadge ? "\n" + line + "\n" : line; } + + log(logObj: LogObject, ctx: { options: ConsolaOptions }) { + // Start spinner + if (logObj.type === "start") { + if (this._spinner) { + this._spinner.stop(); + } + this._spinner = new Spinner( + this.formatLine(logObj, ctx), + ctx.options.stdout, + ); + return; + } + + // Spinner is active + if (this._spinner) { + if (SPINNER_STOP_TYPES.has(logObj.type)) { + // Stop + this._spinner.stop(); + this._spinner = undefined; + } else { + // Spinner interrupted + this._spinner.paused = true; + this._spinner.offset += 1; // this.formatLine(logObj, ctx).split("\n").length; + super.log(logObj, ctx); + this._spinner.paused = false; + return; + } + } + + return super.log(logObj, ctx); + } } function characterFormat(str: string) { diff --git a/src/utils/spinner.ts b/src/utils/spinner.ts new file mode 100644 index 00000000..d3293d85 --- /dev/null +++ b/src/utils/spinner.ts @@ -0,0 +1,57 @@ +import { writeStream } from "./stream"; + +const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"]; + +export class Spinner { + frameIndex: number = 0; + interval?: NodeJS.Timeout; + stream: NodeJS.WriteStream; + offset: number = 0; + paused: boolean = false; + + constructor(message: string = "", stream?: NodeJS.WriteStream) { + this.stream = stream || process.stdout; + + this.write(`${this.getFrame()} ${message}\n`); + this.offset = message.split("\n").length; + + this.interval = setInterval(() => this.render(), 80); + this.interval.unref(); + } + + write(message: string) { + writeStream(message, this.stream); + } + + render() { + if (this.paused) { + return; + } + const frame = this.getFrame(); + return this.write( + this.offset + ? `\u001B[${this.offset}A\r${frame}\u001B[${this.offset}B\r` + : `\r${frame}`, + ); + } + + getFrame() { + return SPINNER_FRAMES[++this.frameIndex % SPINNER_FRAMES.length]; + } + + stop() { + if (this.interval) { + clearInterval(this.interval); + this.interval = undefined; + } + this.stream.write(`\r\u001B[K`); + } +} + +function spyOnStream(stream: NodeJS.WriteStream) { + const write = stream.__write || stream.write; + stream.write = function (chunk: any, ...args: any[]) { + console.log("write", chunk); + return write.call(stream, chunk, ...args); + }; +} diff --git a/src/utils/stream.ts b/src/utils/stream.ts index a8f2cb7b..8e6c3b2c 100644 --- a/src/utils/stream.ts +++ b/src/utils/stream.ts @@ -1,17 +1,18 @@ -/** - * Writes data to a specified NodeJS writable stream. This function supports streams that have a custom - * `__write' method, and will fall back to the default `write' method if `__write' is not present. - * - * @param {any} data - The data to write to the stream. This can be a string, a buffer, or any data type - * supported by the stream's `write' or `__write' method. - * @param {NodeJS.WriteStream} stream - The writable stream to write the data to. This stream - * must implement the `write' method, and can optionally implement a custom `__write' method. - * @returns {boolean} `true` if the data has been completely processed by the write operation, - * indicating that further writes can be performed immediately. Returns `false` if the data is - * buffered by the stream, indicating that the `drain` event should be waited for before writing - * more data. - */ -export function writeStream(data: any, stream: NodeJS.WriteStream) { - const write = (stream as any).__write || stream.write; +import type { WriteStream } from "node:tty"; + +interface ConsolaWriteStream extends WriteStream { + /** patched by consola.wrap*() */ + __write?: WriteStream["write"]; +} + +export function writeStream(data: any, stream: ConsolaWriteStream) { + const write = stream.__write || stream.write; return write.call(stream, data); } + +export function spyOnStream(stream: WriteStream) { + const originalWrite = stream.write; + stream.write = function write(chunk: any, ...args: any[]) { + return originalWrite.call(stream, chunk, ...args); + }; +}