diff --git a/packages/core/package.json b/packages/core/package.json index 84c6d53e..2277de1c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -50,6 +50,7 @@ "test": "vitest run" }, "dependencies": { + "@macfja/ansi": "^1.0.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 7d4afc66..604234e8 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -9,5 +9,5 @@ export { default as Prompt } from './prompts/prompt.js'; export { default as SelectPrompt } from './prompts/select.js'; export { default as SelectKeyPrompt } from './prompts/select-key.js'; export { default as TextPrompt } from './prompts/text.js'; -export { block, isCancel, getColumns } from './utils/index.js'; +export { block, isCancel, getColumns, frameRenderer, appendRenderer } from './utils/index.js'; export { updateSettings, settings } from './utils/settings.js'; diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 089e1dc8..6a85e19c 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -5,7 +5,14 @@ import { Writable } from 'node:stream'; import { cursor, erase } from 'sisteransi'; import wrap from 'wrap-ansi'; -import { CANCEL_SYMBOL, diffLines, isActionKey, setRawMode, settings } from '../utils/index.js'; +import { + CANCEL_SYMBOL, + diffLines, + frameRenderer, + isActionKey, + setRawMode, + settings, +} from '../utils/index.js'; import type { ClackEvents, ClackState } from '../types.js'; import type { Action } from '../utils/index.js'; @@ -28,9 +35,9 @@ export default class Prompt { private rl: ReadLine | undefined; private opts: Omit, 'render' | 'input' | 'output'>; + private renderer: (frame: string) => void; private _render: (context: Omit) => string | undefined; private _track = false; - private _prevFrame = ''; private _subscribers = new Map any; once?: boolean }[]>(); protected _cursor = 0; @@ -51,6 +58,7 @@ export default class Prompt { this.input = input; this.output = output; + this.renderer = frameRenderer(output); } /** @@ -246,51 +254,15 @@ export default class Prompt { this.unsubscribe(); } - private restoreCursor() { - const lines = - wrap(this._prevFrame, process.stdout.columns, { hard: true }).split('\n').length - 1; - this.output.write(cursor.move(-999, lines * -1)); - } - private render() { - const frame = wrap(this._render(this) ?? '', process.stdout.columns, { hard: true }); - if (frame === this._prevFrame) return; - if (this.state === 'initial') { this.output.write(cursor.hide); - } else { - const diff = diffLines(this._prevFrame, frame); - this.restoreCursor(); - // If a single line has changed, only update that line - if (diff && diff?.length === 1) { - const diffLine = diff[0]; - this.output.write(cursor.move(0, diffLine)); - this.output.write(erase.lines(1)); - const lines = frame.split('\n'); - this.output.write(lines[diffLine]); - this._prevFrame = frame; - this.output.write(cursor.move(0, lines.length - diffLine - 1)); - return; - // If many lines have changed, rerender everything past the first line - } - if (diff && diff?.length > 1) { - const diffLine = diff[0]; - this.output.write(cursor.move(0, diffLine)); - this.output.write(erase.down()); - const lines = frame.split('\n'); - const newLines = lines.slice(diffLine); - this.output.write(newLines.join('\n')); - this._prevFrame = frame; - return; - } - - this.output.write(erase.down()); } - this.output.write(frame); + this.renderer(this._render(this) ?? ''); + if (this.state === 'initial') { this.state = 'active'; } - this._prevFrame = frame; } } diff --git a/packages/core/src/utils/display.ts b/packages/core/src/utils/display.ts new file mode 100644 index 00000000..68c72f2c --- /dev/null +++ b/packages/core/src/utils/display.ts @@ -0,0 +1,94 @@ +import type { Writable } from 'node:stream'; +import { stripAnsi, wrap as wrapAnsi } from '@macfja/ansi'; +import { cursor, erase } from 'sisteransi'; +import wrap from 'wrap-ansi'; +import { getColumns } from './index.js'; +import { diffLines } from './string.js'; + +function restoreCursor(output: Writable, prevFrame: string) { + const lines = prevFrame.split('\n').length - 1; + output.write(cursor.move(-999, lines * -1)); +} + +function renderFrame(newFrame: string, prevFrame: string, output: Writable): string { + const frame = wrap(newFrame, getColumns(output), { hard: true }); + if (frame === prevFrame) return frame; + + if (prevFrame === '') { + output.write(frame); + return frame; + } + + const diff = diffLines(prevFrame, frame); + restoreCursor(output, prevFrame); + // If a single line has changed, only update that line + if (diff && diff?.length === 1) { + const diffLine = diff[0]; + output.write(cursor.move(0, diffLine)); + output.write(erase.lines(1)); + const lines = frame.split('\n'); + output.write(lines[diffLine]); + output.write(cursor.move(0, lines.length - diffLine - 1)); + return frame; + } + // If many lines have changed, rerender everything past the first line + if (diff && diff?.length > 1) { + const diffLine = diff[0]; + output.write(cursor.move(0, diffLine)); + output.write(erase.down()); + const lines = frame.split('\n'); + const newLines = lines.slice(diffLine); + output.write(newLines.join('\n')); + return frame; + } + + output.write(erase.down()); + output.write(frame); + + return frame; +} + +/** + * Create a function to render a frame base on the previous call (don't redraw lines that didn't change between 2 calls). + * + * @param output The Writable where to render + * @return The rendering function to call with the new frame to display + */ +export function frameRenderer(output: Writable): (frame: string) => void { + let prevFrame = ''; + return (frame: string) => { + prevFrame = renderFrame(frame, prevFrame, output); + }; +} + +/** + * Create a function to render the next part of a sentence. + * It will automatically wrap (without, if possible, breaking word). + * + * @param output The Writable where to render + * @param joiner The prefix to put in front of each lines + * @param removeLeadingSpace if `true` leading space of new lines will be removed + * @return The rendering function to call with the next part of the content + */ +export function appendRenderer( + output: Writable, + joiner: string, + removeLeadingSpace = true +): (next: string) => void { + let lastLine = joiner; + const joinerLength = stripAnsi(joiner).length + 1; + const newLineRE = removeLeadingSpace ? /\n */g : /\n/g; + + return (next: string) => { + const width = getColumns(output) - joinerLength; + const lines = + lastLine.substring(0, joiner.length) + + wrapAnsi(`${lastLine.substring(joiner.length)}${next}`, width).replace( + newLineRE, + `\n${joiner}` + ); + output?.write(cursor.move(-999, 0) + erase.lines(1)); + output?.write(lines); + lastLine = lines.substring(Math.max(0, lines.lastIndexOf('\n') + 1)); + }; +} diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index b59a5d62..dbd1d7f2 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -8,6 +8,7 @@ import { isActionKey } from './settings.js'; export * from './string.js'; export * from './settings.js'; +export * from './display.js'; const isWindows = globalThis.process.platform.startsWith('win'); diff --git a/packages/prompts/src/spinner.ts b/packages/prompts/src/spinner.ts index 3430c9e7..cd0723c5 100644 --- a/packages/prompts/src/spinner.ts +++ b/packages/prompts/src/spinner.ts @@ -1,6 +1,5 @@ -import { block, settings } from '@clack/core'; +import { block, frameRenderer, settings } from '@clack/core'; import color from 'picocolors'; -import { cursor, erase } from 'sisteransi'; import { type CommonOptions, S_BAR, @@ -34,13 +33,13 @@ export const spinner = ({ const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; const delay = unicode ? 80 : 120; const isCI = process.env.CI === 'true'; + const renderer = frameRenderer(output); let unblock: () => void; let loop: NodeJS.Timeout; let isSpinnerActive = false; let isCancelled = false; let _message = ''; - let _prevMessage: string | undefined = undefined; let _origin: number = performance.now(); const handleExit = (code: number) => { @@ -79,14 +78,6 @@ export const spinner = ({ process.removeListener('exit', handleExit); }; - const clearPrevMessage = () => { - if (_prevMessage === undefined) return; - if (isCI) output.write('\n'); - const prevLines = _prevMessage.split('\n'); - output.write(cursor.move(-999, prevLines.length - 1)); - output.write(erase.down(prevLines.length)); - }; - const removeTrailingDots = (msg: string): string => { return msg.replace(/\.+$/, ''); }; @@ -108,20 +99,15 @@ export const spinner = ({ let indicatorTimer = 0; registerHooks(); loop = setInterval(() => { - if (isCI && _message === _prevMessage) { - return; - } - clearPrevMessage(); - _prevMessage = _message; const frame = color.magenta(frames[frameIndex]); if (isCI) { - output.write(`${frame} ${_message}...`); + renderer(`${frame} ${_message}...`); } else if (indicator === 'timer') { - output.write(`${frame} ${_message} ${formatTimer(_origin)}`); + renderer(`${frame} ${_message} ${formatTimer(_origin)}`); } else { const loadingDots = '.'.repeat(Math.floor(indicatorTimer)).slice(0, 3); - output.write(`${frame} ${_message}${loadingDots}`); + renderer(`${frame} ${_message}${loadingDots}`); } frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; @@ -132,7 +118,6 @@ export const spinner = ({ const stop = (msg = '', code = 0): void => { isSpinnerActive = false; clearInterval(loop); - clearPrevMessage(); const step = code === 0 ? color.green(S_STEP_SUBMIT) @@ -141,10 +126,11 @@ export const spinner = ({ : color.red(S_STEP_ERROR); _message = msg ?? _message; if (indicator === 'timer') { - output.write(`${step} ${_message} ${formatTimer(_origin)}\n`); + renderer(`${step} ${_message} ${formatTimer(_origin)}`); } else { - output.write(`${step} ${_message}\n`); + renderer(`${step} ${_message}`); } + output?.write('\n'); clearHooks(); unblock(); }; diff --git a/packages/prompts/src/stream.ts b/packages/prompts/src/stream.ts index 19e79c11..afba911f 100644 --- a/packages/prompts/src/stream.ts +++ b/packages/prompts/src/stream.ts @@ -1,37 +1,21 @@ -import { stripVTControlCharacters as strip } from 'node:util'; +import { appendRenderer } from '@clack/core'; import color from 'picocolors'; import { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT, S_SUCCESS, S_WARN } from './common.js'; import type { LogMessageOptions } from './log.js'; const prefix = `${color.gray(S_BAR)} `; -// TODO (43081j): this currently doesn't support custom `output` writables -// because we rely on `columns` existing (i.e. `process.stdout.columns). -// -// If we want to support `output` being passed in, we will need to use -// a condition like `if (output insance Writable)` to check if it has columns export const stream = { message: async ( iterable: Iterable | AsyncIterable, - { symbol = color.gray(S_BAR) }: LogMessageOptions = {} + { symbol = color.gray(S_BAR), output = process.stdout }: LogMessageOptions = {} ) => { - process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `); - let lineWidth = 3; - for await (let chunk of iterable) { - chunk = chunk.replace(/\n/g, `\n${prefix}`); - if (chunk.includes('\n')) { - lineWidth = 3 + strip(chunk.slice(chunk.lastIndexOf('\n'))).length; - } - const chunkLen = strip(chunk).length; - if (lineWidth + chunkLen < process.stdout.columns) { - lineWidth += chunkLen; - process.stdout.write(chunk); - } else { - process.stdout.write(`\n${prefix}${chunk.trimStart()}`); - lineWidth = 3 + strip(chunk.trimStart()).length; - } + output.write(`${color.gray(S_BAR)}\n${symbol} `); + const renderer = appendRenderer(output, prefix); + for await (const chunk of iterable) { + renderer(chunk); } - process.stdout.write('\n'); + output.write('\n'); }, info: (iterable: Iterable | AsyncIterable) => { return stream.message(iterable, { symbol: color.blue(S_INFO) }); diff --git a/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap b/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap index accb85b6..aec4c253 100644 --- a/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap +++ b/packages/prompts/test/__snapshots__/progress-bar.test.ts.snap @@ -5,10 +5,12 @@ exports[`prompts - progress (isCI = false) > message > sets message for next fra "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", + "", + "", "◐ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ foo", + "", ] `; @@ -17,7 +19,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > prioritizes "[?25l", "│ ", - "■ Progress cancel message + "■ Progress cancel message", + " ", "[?25h", ] @@ -28,7 +31,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > prioritizes "[?25l", "│ ", - "▲ Progress error message + "▲ Progress error message", + " ", "[?25h", ] @@ -39,7 +43,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > uses custom "[?25l", "│ ", - "■ Custom cancel message + "■ Custom cancel message", + " ", "[?25h", ] @@ -50,7 +55,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > uses custom "[?25l", "│ ", - "▲ Custom error message + "▲ Custom error message", + " ", "[?25h", ] @@ -61,7 +67,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > uses defaul "[?25l", "│ ", - "■ Canceled + "■ Canceled", + " ", "[?25h", ] @@ -72,7 +79,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > uses global "[?25l", "│ ", - "■ Global cancel message + "■ Global cancel message", + " ", "[?25h", ] @@ -83,7 +91,8 @@ exports[`prompts - progress (isCI = false) > process exit handling > uses global "[?25l", "│ ", - "▲ Global error message + "▲ Global error message", + " ", "[?25h", ] @@ -94,16 +103,22 @@ exports[`prompts - progress (isCI = false) > start > renders frames at interval "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "◐ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "", + "", + "◐ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "", "", - "", - "◓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "", + "", + "◓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "", "", - "", - "◑ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "", + "", + "◑ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", + "", ] `; @@ -130,10 +145,13 @@ exports[`prompts - progress (isCI = false) > stop > renders cancel symbol if cod "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "■ + "", + "", + "■", + "", + " ", "[?25h", ] @@ -144,10 +162,13 @@ exports[`prompts - progress (isCI = false) > stop > renders error symbol if code "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "▲ + "", + "", + "▲", + "", + " ", "[?25h", ] @@ -158,10 +179,13 @@ exports[`prompts - progress (isCI = false) > stop > renders message 1`] = ` "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "◇ foo + "", + "", + "◇ foo", + "", + " ", "[?25h", ] @@ -172,10 +196,13 @@ exports[`prompts - progress (isCI = false) > stop > renders message without remo "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "◇ foo. + "", + "", + "◇ foo.", + "", + " ", "[?25h", ] @@ -186,10 +213,13 @@ exports[`prompts - progress (isCI = false) > stop > renders submit symbol and st "[?25l", "│ ", - "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ", + "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -200,19 +230,28 @@ exports[`prompts - progress (isCI = false) > style > renders block progressbar 1 "[?25l", "│ ", - "◒ ██████████ ", + "◒ ██████████", "", - "", - "◐ ██████████ ", + "", + "", + "◐ ██████████", + "", "", - "", - "◓ ██████████ ", + "", + "", + "◓ ██████████", + "", "", - "", - "◑ ██████████ ", + "", + "", + "◑ ██████████", + "", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -223,19 +262,28 @@ exports[`prompts - progress (isCI = false) > style > renders heavy progressbar 1 "[?25l", "│ ", - "◒ ━━━━━━━━━━ ", + "◒ ━━━━━━━━━━", "", - "", - "◐ ━━━━━━━━━━ ", + "", + "", + "◐ ━━━━━━━━━━", + "", "", - "", - "◓ ━━━━━━━━━━ ", + "", + "", + "◓ ━━━━━━━━━━", + "", "", - "", - "◑ ━━━━━━━━━━ ", + "", + "", + "◑ ━━━━━━━━━━", + "", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -246,19 +294,28 @@ exports[`prompts - progress (isCI = false) > style > renders light progressbar 1 "[?25l", "│ ", - "◒ ────────── ", + "◒ ──────────", "", - "", - "◐ ────────── ", + "", + "", + "◐ ──────────", + "", "", - "", - "◓ ────────── ", + "", + "", + "◓ ──────────", + "", "", - "", - "◑ ────────── ", + "", + "", + "◑ ──────────", + "", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -270,11 +327,11 @@ exports[`prompts - progress (isCI = true) > message > sets message for next fram "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", + "", + "", "◐ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ foo...", + "", ] `; @@ -283,7 +340,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > prioritizes "[?25l", "│ ", - "■ Progress cancel message + "■ Progress cancel message", + " ", "[?25h", ] @@ -294,7 +352,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > prioritizes "[?25l", "│ ", - "▲ Progress error message + "▲ Progress error message", + " ", "[?25h", ] @@ -305,7 +364,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > uses custom "[?25l", "│ ", - "■ Custom cancel message + "■ Custom cancel message", + " ", "[?25h", ] @@ -316,7 +376,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > uses custom "[?25l", "│ ", - "▲ Custom error message + "▲ Custom error message", + " ", "[?25h", ] @@ -327,7 +388,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > uses default "[?25l", "│ ", - "■ Canceled + "■ Canceled", + " ", "[?25h", ] @@ -338,7 +400,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > uses global "[?25l", "│ ", - "■ Global cancel message + "■ Global cancel message", + " ", "[?25h", ] @@ -349,7 +412,8 @@ exports[`prompts - progress (isCI = true) > process exit handling > uses global "[?25l", "│ ", - "▲ Global error message + "▲ Global error message", + " ", "[?25h", ] @@ -361,6 +425,21 @@ exports[`prompts - progress (isCI = true) > start > renders frames at interval 1 "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + "", + "", + "", + "◐ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + "", + "", + "", + "", + "◓ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + "", + "", + "", + "", + "◑ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", + "", ] `; @@ -388,11 +467,12 @@ exports[`prompts - progress (isCI = true) > stop > renders cancel symbol if code "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", - "■ + "", + "", + "■", + "", + " ", "[?25h", ] @@ -404,11 +484,12 @@ exports[`prompts - progress (isCI = true) > stop > renders error symbol if code "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", - "▲ + "", + "", + "▲", + "", + " ", "[?25h", ] @@ -420,11 +501,12 @@ exports[`prompts - progress (isCI = true) > stop > renders message 1`] = ` "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", - "◇ foo + "", + "", + "◇ foo", + "", + " ", "[?25h", ] @@ -436,11 +518,12 @@ exports[`prompts - progress (isCI = true) > stop > renders message without remov "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", - "◇ foo. + "", + "", + "◇ foo.", + "", + " ", "[?25h", ] @@ -452,11 +535,12 @@ exports[`prompts - progress (isCI = true) > stop > renders submit symbol and sto "│ ", "◒ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ...", - " -", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -468,16 +552,27 @@ exports[`prompts - progress (isCI = true) > style > renders block progressbar 1` "│ ", "◒ ██████████ ...", - " -", "", - "", - "◐ ██████████ ...", + "", + "", + "◐ ██████████ ...", + "", + "", + "", + "", + "◓ ██████████ ...", + "", + "", + "", + "", + "◑ ██████████ ...", + "", + "", + "", + "", + "◇", + "", " -", - "", - "", - "◇ ", "[?25h", ] @@ -489,16 +584,27 @@ exports[`prompts - progress (isCI = true) > style > renders heavy progressbar 1` "│ ", "◒ ━━━━━━━━━━ ...", - " -", "", - "", - "◐ ━━━━━━━━━━ ...", + "", + "", + "◐ ━━━━━━━━━━ ...", + "", + "", + "", + "", + "◓ ━━━━━━━━━━ ...", + "", + "", + "", + "", + "◑ ━━━━━━━━━━ ...", + "", + "", + "", + "", + "◇", + "", " -", - "", - "", - "◇ ", "[?25h", ] @@ -510,16 +616,27 @@ exports[`prompts - progress (isCI = true) > style > renders light progressbar 1` "│ ", "◒ ────────── ...", - " -", "", - "", - "◐ ────────── ...", + "", + "", + "◐ ────────── ...", + "", + "", + "", + "", + "◓ ────────── ...", + "", + "", + "", + "", + "◑ ────────── ...", + "", + "", + "", + "", + "◇", + "", " -", - "", - "", - "◇ ", "[?25h", ] diff --git a/packages/prompts/test/__snapshots__/spinner.test.ts.snap b/packages/prompts/test/__snapshots__/spinner.test.ts.snap index abc374a1..32584e68 100644 --- a/packages/prompts/test/__snapshots__/spinner.test.ts.snap +++ b/packages/prompts/test/__snapshots__/spinner.test.ts.snap @@ -5,10 +5,12 @@ exports[`spinner (isCI = false) > message > sets message for next frame 1`] = ` "[?25l", "│ ", - "◒ ", + "◒", "", - "", + "", + "", "◐ foo", + "", ] `; @@ -17,7 +19,8 @@ exports[`spinner (isCI = false) > process exit handling > prioritizes direct opt "[?25l", "│ ", - "■ Spinner cancel message + "■ Spinner cancel message", + " ", "[?25h", ] @@ -28,7 +31,8 @@ exports[`spinner (isCI = false) > process exit handling > prioritizes direct opt "[?25l", "│ ", - "▲ Spinner error message + "▲ Spinner error message", + " ", "[?25h", ] @@ -39,7 +43,8 @@ exports[`spinner (isCI = false) > process exit handling > uses custom cancel mes "[?25l", "│ ", - "■ Custom cancel message + "■ Custom cancel message", + " ", "[?25h", ] @@ -50,7 +55,8 @@ exports[`spinner (isCI = false) > process exit handling > uses custom error mess "[?25l", "│ ", - "▲ Custom error message + "▲ Custom error message", + " ", "[?25h", ] @@ -61,7 +67,8 @@ exports[`spinner (isCI = false) > process exit handling > uses default cancel me "[?25l", "│ ", - "■ Canceled + "■ Canceled", + " ", "[?25h", ] @@ -72,7 +79,8 @@ exports[`spinner (isCI = false) > process exit handling > uses global custom can "[?25l", "│ ", - "■ Global cancel message + "■ Global cancel message", + " ", "[?25h", ] @@ -83,7 +91,8 @@ exports[`spinner (isCI = false) > process exit handling > uses global custom err "[?25l", "│ ", - "▲ Global error message + "▲ Global error message", + " ", "[?25h", ] @@ -94,16 +103,22 @@ exports[`spinner (isCI = false) > start > renders frames at interval 1`] = ` "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "◐ ", + "", + "", + "◐", + "", "", - "", - "◓ ", + "", + "", + "◓", + "", "", - "", - "◑ ", + "", + "", + "◑", + "", ] `; @@ -130,10 +145,13 @@ exports[`spinner (isCI = false) > stop > renders cancel symbol if code = 1 1`] = "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "■ + "", + "", + "■", + "", + " ", "[?25h", ] @@ -144,10 +162,13 @@ exports[`spinner (isCI = false) > stop > renders error symbol if code > 1 1`] = "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "▲ + "", + "", + "▲", + "", + " ", "[?25h", ] @@ -158,10 +179,13 @@ exports[`spinner (isCI = false) > stop > renders message 1`] = ` "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "◇ foo + "", + "", + "◇ foo", + "", + " ", "[?25h", ] @@ -172,10 +196,13 @@ exports[`spinner (isCI = false) > stop > renders message without removing dots 1 "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "◇ foo. + "", + "", + "◇ foo.", + "", + " ", "[?25h", ] @@ -186,10 +213,13 @@ exports[`spinner (isCI = false) > stop > renders submit symbol and stops spinner "[?25l", "│ ", - "◒ ", + "◒", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] @@ -201,11 +231,11 @@ exports[`spinner (isCI = true) > message > sets message for next frame 1`] = ` "│ ", "◒ ...", - " -", "", - "", + "", + "", "◐ foo...", + "", ] `; @@ -214,7 +244,8 @@ exports[`spinner (isCI = true) > process exit handling > prioritizes direct opti "[?25l", "│ ", - "■ Spinner cancel message + "■ Spinner cancel message", + " ", "[?25h", ] @@ -225,7 +256,8 @@ exports[`spinner (isCI = true) > process exit handling > prioritizes direct opti "[?25l", "│ ", - "▲ Spinner error message + "▲ Spinner error message", + " ", "[?25h", ] @@ -236,7 +268,8 @@ exports[`spinner (isCI = true) > process exit handling > uses custom cancel mess "[?25l", "│ ", - "■ Custom cancel message + "■ Custom cancel message", + " ", "[?25h", ] @@ -247,7 +280,8 @@ exports[`spinner (isCI = true) > process exit handling > uses custom error messa "[?25l", "│ ", - "▲ Custom error message + "▲ Custom error message", + " ", "[?25h", ] @@ -258,7 +292,8 @@ exports[`spinner (isCI = true) > process exit handling > uses default cancel mes "[?25l", "│ ", - "■ Canceled + "■ Canceled", + " ", "[?25h", ] @@ -269,7 +304,8 @@ exports[`spinner (isCI = true) > process exit handling > uses global custom canc "[?25l", "│ ", - "■ Global cancel message + "■ Global cancel message", + " ", "[?25h", ] @@ -280,7 +316,8 @@ exports[`spinner (isCI = true) > process exit handling > uses global custom erro "[?25l", "│ ", - "▲ Global error message + "▲ Global error message", + " ", "[?25h", ] @@ -292,6 +329,21 @@ exports[`spinner (isCI = true) > start > renders frames at interval 1`] = ` "│ ", "◒ ...", + "", + "", + "", + "◐ ...", + "", + "", + "", + "", + "◓ ...", + "", + "", + "", + "", + "◑ ...", + "", ] `; @@ -319,11 +371,12 @@ exports[`spinner (isCI = true) > stop > renders cancel symbol if code = 1 1`] = "│ ", "◒ ...", - " -", "", - "", - "■ + "", + "", + "■", + "", + " ", "[?25h", ] @@ -335,11 +388,12 @@ exports[`spinner (isCI = true) > stop > renders error symbol if code > 1 1`] = ` "│ ", "◒ ...", - " -", "", - "", - "▲ + "", + "", + "▲", + "", + " ", "[?25h", ] @@ -351,11 +405,12 @@ exports[`spinner (isCI = true) > stop > renders message 1`] = ` "│ ", "◒ ...", - " -", "", - "", - "◇ foo + "", + "", + "◇ foo", + "", + " ", "[?25h", ] @@ -367,11 +422,12 @@ exports[`spinner (isCI = true) > stop > renders message without removing dots 1` "│ ", "◒ ...", - " -", "", - "", - "◇ foo. + "", + "", + "◇ foo.", + "", + " ", "[?25h", ] @@ -383,11 +439,12 @@ exports[`spinner (isCI = true) > stop > renders submit symbol and stops spinner "│ ", "◒ ...", - " -", "", - "", - "◇ + "", + "", + "◇", + "", + " ", "[?25h", ] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53555bcb..a8fd1af3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -61,6 +61,9 @@ importers: packages/core: dependencies: + '@macfja/ansi': + specifier: ^1.0.0 + version: 1.0.0 picocolors: specifier: ^1.0.0 version: 1.0.0 @@ -726,6 +729,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@macfja/ansi@1.0.0': + resolution: {integrity: sha512-fcshx5NS+PFmIZL2UiA+kowuli0LZYxV+gVnzZowvQTEh/3QVOvs19VcsyZG55RfVJMFz7cbwwTaN5yrpvk5tQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2667,6 +2673,10 @@ packages: regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.escape@2.0.1: + resolution: {integrity: sha512-JItRb4rmyTzmERBkAf6J87LjDPy/RscIwmaJQ3gsFlAzrmZbZU8LwBw5IydFZXW9hqpgbPlGbMhtpqtuAhMgtg==} + engines: {node: '>= 0.4'} + regexp.prototype.flags@1.5.3: resolution: {integrity: sha512-vqlC04+RQoFalODCbCumG2xIOvapzVMHwsyIGM/SIE8fRhFFsXeH8/QQ+s0T0kDAhKc4k30s73/0ydkHQz6HlQ==} engines: {node: '>= 0.4'} @@ -3195,6 +3205,10 @@ packages: engines: {node: '>=8'} hasBin: true + wordwrapjs@5.1.0: + resolution: {integrity: sha512-JNjcULU2e4KJwUNv6CHgI46UvDGitb6dGryHajXTDiLgg1/RiGoPSDw4kZfYnwGtEXf2ZMeIewDQgFGzkCB2Sg==} + engines: {node: '>=12.17'} + wrap-ansi@6.2.0: resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} engines: {node: '>=8'} @@ -3816,6 +3830,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@macfja/ansi@1.0.0': + dependencies: + regexp.escape: 2.0.1 + wordwrapjs: 5.1.0 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.26.0 @@ -5864,6 +5883,15 @@ snapshots: regenerator-runtime@0.14.1: {} + regexp.escape@2.0.1: + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.5 + es-errors: 1.3.0 + for-each: 0.3.3 + safe-regex-test: 1.0.3 + regexp.prototype.flags@1.5.3: dependencies: call-bind: 1.0.7 @@ -6463,6 +6491,8 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wordwrapjs@5.1.0: {} + wrap-ansi@6.2.0: dependencies: ansi-styles: 4.3.0