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: animated spinner #334

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions examples/spinner.ts
Original file line number Diff line number Diff line change
@@ -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!");
7 changes: 5 additions & 2 deletions src/reporters/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
41 changes: 39 additions & 2 deletions src/reporters/fancy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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";
Expand All @@ -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 (
Expand Down Expand Up @@ -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) {
Expand Down
57 changes: 57 additions & 0 deletions src/utils/spinner.ts
Original file line number Diff line number Diff line change
@@ -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);
};
}
31 changes: 16 additions & 15 deletions src/utils/stream.ts
Original file line number Diff line number Diff line change
@@ -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);
};
}
Loading