From 720ca54df930e6d0f8076a8bcbf991f1b03f934d Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 14 Aug 2023 14:15:33 -0300 Subject: [PATCH 01/20] feat: multiline draft --- packages/core/src/prompts/prompt.ts | 147 ++++++++++++++++++++++++++++ packages/core/src/utils/index.ts | 20 ++++ packages/prompts/src/index.ts | 72 ++++++++++++-- 3 files changed, 230 insertions(+), 9 deletions(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index f2c85771..6c41bb35 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -4,6 +4,7 @@ import type { Readable, Writable } from 'node:stream'; import { WriteStream } from 'node:tty'; import { cursor, erase } from 'sisteransi'; import wrap from 'wrap-ansi'; +import { strLength } from '../utils'; import { ALIASES, CANCEL_SYMBOL, KEYS, diffLines, hasAliasKey, setRawMode } from '../utils'; @@ -19,6 +20,89 @@ export interface PromptOptions { debug?: boolean; } +export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error'; + +export type LineOption = 'firstLine' | 'newLine' | 'lastLine'; + +export interface FormatLineOptions { + /** + * Define the start of line + * @example + * format('foo', { + * line: { + * start: '-' + * } + * }) + * //=> '- foo' + */ + start: string; + /** + * Define the end of line + * @example + * format('foo', { + * line: { + * end: '-' + * } + * }) + * //=> 'foo -' + */ + end: string; + /** + * Define the sides of line + * @example + * format('foo', { + * line: { + * sides: '-' + * } + * }) + * //=> '- foo -' + */ + sides: string; + /** + * Define the style of line + * @example + * format('foo', { + * line: { + * style: (line) => `(${line})` + * } + * }) + * //=> '(foo)' + */ + style: (line: string) => string; +} + +export interface FormatOptions extends Record> { + /** + * Shorthand to define values for each line + * @example + * format('foo', { + * default: { + * start: '-' + * } + * // equals + * firstLine{ + * start: '-' + * }, + * newLine{ + * start: '-' + * }, + * lastLine{ + * start: '-' + * }, + * }) + */ + default: Partial; + /** + * Define the max width of each line + * @example + * format('foo bar baz', { + * maxWidth: 7 + * }) + * //=> 'foo bar\nbaz' + */ + maxWidth: number; +} + export default class Prompt { protected input: Readable; protected output: Writable; @@ -221,6 +305,69 @@ export default class Prompt { this.output.write(cursor.move(-999, lines * -1)); } + public format(text: string, options?: Partial): string { + const getLineOption = ( + line: TLine, + key: TKey + ): NonNullable => { + return ( + key === 'style' + ? options?.[line]?.[key] ?? ((line) => line) + : options?.[line]?.[key] ?? options?.[line]?.sides ?? options?.default?.[key] ?? '' + ) as NonNullable; + }; + const getLineOptions = (line: LineOption): Omit => { + return { + start: getLineOption(line, 'start'), + end: getLineOption(line, 'end'), + style: getLineOption(line, 'style'), + }; + }; + + const firstLine = getLineOptions('firstLine'); + const newLine = getLineOptions('newLine'); + const lastLine = getLineOptions('lastLine'); + + const terminalWidth = process.stdout.columns || 80; + const maxWidth = options?.maxWidth ?? terminalWidth - 2; + + const formattedLines: string[] = []; + const paragraphs = text.split(/\n/g); + + for (let i = 0; i < paragraphs.length; i++) { + const opt = >( + position: TPosition + ): FormatLineOptions[TPosition] => { + return ( + i === 0 + ? firstLine[position] + : i + 1 === paragraphs.length + ? lastLine[position] + : newLine[position] + ) as FormatLineOptions[TPosition]; + }; + const startLine = opt('start'); + const endLine = opt('end'); + const styleLine = opt('style'); + let currentLine = ' '; + + const words = paragraphs[i].split(/\s/g); + for (const word of words) { + if (strLength(startLine + currentLine + word + endLine) + 3 <= maxWidth) { + currentLine += ` ${word}`; + } else { + formattedLines.push([startLine, styleLine(currentLine), endLine].join(' ')); + currentLine = word; + } + } + + formattedLines.push([startLine, styleLine(currentLine), endLine].join(' ')); + } + + return formattedLines.join('\n'); + } + + private _prevFrame = ''; private render() { const frame = wrap(this._render(this) ?? '', process.stdout.columns, { hard: true }); if (frame === this._prevFrame) return; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index ef0707c8..239ee815 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -69,3 +69,23 @@ export function block({ rl.close(); }; } + +export function strLength(str: string) { + if (!str) return 0; + + const colorCodeRegex = /\x1B\[[0-9;]*[mG]/g; + const ansiRegex = new RegExp( + [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', + ].join('|'), + 'g' + ); + const arr = [...str.replace(colorCodeRegex, '').replace(ansiRegex, '')]; + let len = 0; + + for (const char of arr) { + len += char.charCodeAt(0) > 127 || char.charCodeAt(0) === 94 ? 2 : 1; + } + return len; +} diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2d753648..f6a9c374 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -109,7 +109,17 @@ export const text = (opts: TextOptions) => { defaultValue: opts.defaultValue, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const title = [ + color.gray(S_BAR), + this.format(opts.message, { + firstLine: { + start: symbol(this.state), + }, + default: { + start: color.gray(S_BAR), + }, + }), + ].join('\n'); const placeholder = opts.placeholder ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) : color.inverse(color.hidden('_')); @@ -117,17 +127,61 @@ export const text = (opts: TextOptions) => { switch (this.state) { case 'error': - return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow( - S_BAR_END - )} ${color.yellow(this.error)}\n`; + return [ + title, + this.format(value, { + default: { + start: color.yellow(S_BAR), + }, + lastLine: { + start: color.yellow(S_BAR_END), + }, + }), + this.format(this.error, { + default: { + start: color.yellow(S_BAR), + style: color.yellow, + }, + lastLine: { + start: color.yellow(S_BAR_END), + }, + }), + ].join('\n'); case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(this.value || opts.placeholder)}`; + return [ + title, + this.format(this.value ?? opts.placeholder, { + default: { + start: color.gray(S_BAR), + style: color.dim, + }, + }), + ].join('\n'); case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough( - color.dim(this.value ?? '') - )}${this.value?.trim() ? `\n${color.gray(S_BAR)}` : ''}`; + return [ + title, + this.format(this.value ?? '', { + default: { + start: color.gray(S_BAR), + style: (line) => color.strikethrough(color.dim(line)), + }, + lastLine: { + start: color.gray(S_BAR_END), + }, + }), + ].join('\n'); default: - return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; + return [ + title, + this.format(value, { + default: { + start: color.cyan(S_BAR), + }, + lastLine: { + start: color.cyan(S_BAR_END), + }, + }), + ].join('\n'); } }, }).prompt() as Promise; From 50f586e5c3ea79757d7dcdc6006761326e5f6f35 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 14 Aug 2023 17:35:36 -0300 Subject: [PATCH 02/20] fix: format single line --- packages/core/src/prompts/prompt.ts | 51 ++++++++++++++++++----------- 1 file changed, 32 insertions(+), 19 deletions(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 6c41bb35..3f1d77ea 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -328,43 +328,56 @@ export default class Prompt { const newLine = getLineOptions('newLine'); const lastLine = getLineOptions('lastLine'); + const emptySlots = Math.max( + strLength(firstLine.start + firstLine.end), + strLength(newLine.start + newLine.end), + strLength(lastLine.start + lastLine.end) + ); const terminalWidth = process.stdout.columns || 80; - const maxWidth = options?.maxWidth ?? terminalWidth - 2; + const maxWidth = (options?.maxWidth ?? terminalWidth) - emptySlots; const formattedLines: string[] = []; const paragraphs = text.split(/\n/g); for (let i = 0; i < paragraphs.length; i++) { - const opt = >( - position: TPosition - ): FormatLineOptions[TPosition] => { - return ( - i === 0 - ? firstLine[position] - : i + 1 === paragraphs.length - ? lastLine[position] - : newLine[position] - ) as FormatLineOptions[TPosition]; - }; - const startLine = opt('start'); - const endLine = opt('end'); - const styleLine = opt('style'); let currentLine = ' '; const words = paragraphs[i].split(/\s/g); for (const word of words) { - if (strLength(startLine + currentLine + word + endLine) + 3 <= maxWidth) { + if (strLength(currentLine + word) + emptySlots + 3 <= maxWidth) { currentLine += ` ${word}`; } else { - formattedLines.push([startLine, styleLine(currentLine), endLine].join(' ')); + formattedLines.push(currentLine); currentLine = word; } } - formattedLines.push([startLine, styleLine(currentLine), endLine].join(' ')); + formattedLines.push(currentLine); } - return formattedLines.join('\n'); + return formattedLines + .map((line, i, ar) => { + const opt = >( + position: TPosition + ): FormatLineOptions[TPosition] => { + return ( + i === 0 && ar.length === 1 + ? options?.firstLine?.[position] ?? + options?.lastLine?.[position] ?? + firstLine[position] + : i === 0 + ? firstLine[position] + : i + 1 === ar.length + ? lastLine[position] + : newLine[position] + ) as FormatLineOptions[TPosition]; + }; + const startLine = opt('start'); + const endLine = opt('end'); + const styleLine = opt('style'); + return [startLine, styleLine(line), endLine].join(' '); + }) + .join('\n'); } private _prevFrame = ''; From 74d2b7d09e04fa780965ce83e37073fa2e85f3ab Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 15 Aug 2023 20:13:22 -0300 Subject: [PATCH 03/20] feat: inner line break --- packages/core/src/prompts/prompt.ts | 28 ++++++++++++++++++++-------- packages/prompts/src/index.ts | 20 ++++++++++---------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 3f1d77ea..47005650 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -328,24 +328,36 @@ export default class Prompt { const newLine = getLineOptions('newLine'); const lastLine = getLineOptions('lastLine'); - const emptySlots = Math.max( - strLength(firstLine.start + firstLine.end), - strLength(newLine.start + newLine.end), - strLength(lastLine.start + lastLine.end) - ); + const emptySlots = + Math.max( + strLength(firstLine.start + firstLine.end), + strLength(newLine.start + newLine.end), + strLength(lastLine.start + lastLine.end) + ) + 2; const terminalWidth = process.stdout.columns || 80; - const maxWidth = (options?.maxWidth ?? terminalWidth) - emptySlots; + const maxWidth = options?.maxWidth ?? terminalWidth; const formattedLines: string[] = []; const paragraphs = text.split(/\n/g); for (let i = 0; i < paragraphs.length; i++) { - let currentLine = ' '; + let currentLine = ''; const words = paragraphs[i].split(/\s/g); for (const word of words) { - if (strLength(currentLine + word) + emptySlots + 3 <= maxWidth) { + if (strLength(currentLine + word) + emptySlots + 1 <= maxWidth) { currentLine += ` ${word}`; + } else if (strLength(word) + emptySlots >= maxWidth) { + const splitIndex = maxWidth - strLength(currentLine) - emptySlots - 1; + formattedLines.push(currentLine + ' ' + word.slice(0, splitIndex)); + + const chunkLength = maxWidth - emptySlots; + let chunk = word.slice(splitIndex); + while (strLength(chunk) >= chunkLength) { + formattedLines.push(chunk.slice(0, chunkLength)); + chunk = chunk.slice(chunkLength); + } + currentLine = chunk; } else { formattedLines.push(currentLine); currentLine = word; diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index f6a9c374..9b611e04 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -133,9 +133,6 @@ export const text = (opts: TextOptions) => { default: { start: color.yellow(S_BAR), }, - lastLine: { - start: color.yellow(S_BAR_END), - }, }), this.format(this.error, { default: { @@ -165,22 +162,25 @@ export const text = (opts: TextOptions) => { start: color.gray(S_BAR), style: (line) => color.strikethrough(color.dim(line)), }, - lastLine: { - start: color.gray(S_BAR_END), - }, }), ].join('\n'); default: return [ - title, - this.format(value, { + color.gray(S_BAR), + this.format(opts.message, { + firstLine: { + start: symbol(this.state), + }, default: { start: color.cyan(S_BAR), }, - lastLine: { - start: color.cyan(S_BAR_END), + }), + this.format(value, { + default: { + start: color.cyan(S_BAR), }, }), + color.cyan(S_BAR_END), ].join('\n'); } }, From dae4374aae1a1465ba89db4fcd38519b8849f1d9 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 09:44:17 -0300 Subject: [PATCH 04/20] fix: style format --- packages/core/src/prompts/prompt.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 47005650..432f6c3a 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -312,7 +312,7 @@ export default class Prompt { ): NonNullable => { return ( key === 'style' - ? options?.[line]?.[key] ?? ((line) => line) + ? options?.[line]?.[key] ?? options?.default?.[key] ?? ((line) => line) : options?.[line]?.[key] ?? options?.[line]?.sides ?? options?.default?.[key] ?? '' ) as NonNullable; }; @@ -340,10 +340,10 @@ export default class Prompt { const formattedLines: string[] = []; const paragraphs = text.split(/\n/g); - for (let i = 0; i < paragraphs.length; i++) { + for (const paragraph of paragraphs) { + const words = paragraph.split(/\s/g); let currentLine = ''; - const words = paragraphs[i].split(/\s/g); for (const word of words) { if (strLength(currentLine + word) + emptySlots + 1 <= maxWidth) { currentLine += ` ${word}`; @@ -353,7 +353,7 @@ export default class Prompt { const chunkLength = maxWidth - emptySlots; let chunk = word.slice(splitIndex); - while (strLength(chunk) >= chunkLength) { + while (strLength(chunk) > chunkLength) { formattedLines.push(chunk.slice(0, chunkLength)); chunk = chunk.slice(chunkLength); } @@ -387,7 +387,12 @@ export default class Prompt { const startLine = opt('start'); const endLine = opt('end'); const styleLine = opt('style'); - return [startLine, styleLine(line), endLine].join(' '); + // only format the line without the leading space. + const leadingSpaceRegex = /^\s/; + const styledLine = leadingSpaceRegex.test(line) + ? ' ' + styleLine(line.slice(1)) + : styleLine(line); + return [startLine, styleLine(styledLine), endLine].join(' '); }) .join('\n'); } From 53b144d9b78fc22bab7c8879704eff0c627f6620 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:21:32 -0300 Subject: [PATCH 05/20] feat: format with minWith --- packages/core/src/prompts/prompt.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 432f6c3a..e1802db3 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -101,6 +101,7 @@ export interface FormatOptions extends Record 'foo bar\nbaz' */ maxWidth: number; + minWidth: number; } export default class Prompt { @@ -336,6 +337,7 @@ export default class Prompt { ) + 2; const terminalWidth = process.stdout.columns || 80; const maxWidth = options?.maxWidth ?? terminalWidth; + const minWidth = options?.minWidth ?? 1; const formattedLines: string[] = []; const paragraphs = text.split(/\n/g); @@ -392,7 +394,9 @@ export default class Prompt { const styledLine = leadingSpaceRegex.test(line) ? ' ' + styleLine(line.slice(1)) : styleLine(line); - return [startLine, styleLine(styledLine), endLine].join(' '); + const fullLine = + styledLine + ' '.repeat(Math.max(minWidth - strLength(styledLine) - emptySlots, 0)); + return [startLine, fullLine, endLine].join(' '); }) .join('\n'); } From 250ab45947ae98ece308649d16a18b0fbc17c60c Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:30:07 -0300 Subject: [PATCH 06/20] feat: default theme --- packages/core/src/themes/default.ts | 105 ++++++++++++++++++++++++++++ packages/core/src/themes/index.ts | 3 + packages/core/src/themes/symbols.ts | 30 ++++++++ packages/core/src/themes/types.ts | 10 +++ 4 files changed, 148 insertions(+) create mode 100644 packages/core/src/themes/default.ts create mode 100644 packages/core/src/themes/index.ts create mode 100644 packages/core/src/themes/symbols.ts create mode 100644 packages/core/src/themes/types.ts diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts new file mode 100644 index 00000000..f8529218 --- /dev/null +++ b/packages/core/src/themes/default.ts @@ -0,0 +1,105 @@ +import Prompt, { State } from '../prompts/prompt'; +import { TemplateOptions } from './types'; +import * as S from './symbols'; +import color from 'picocolors'; + +const symbol = (state: State) => { + switch (state) { + case 'initial': + case 'active': + return color.cyan(S.STEP_ACTIVE); + case 'cancel': + return color.red(S.STEP_CANCEL); + case 'error': + return color.yellow(S.STEP_ERROR); + case 'submit': + return color.green(S.STEP_SUBMIT); + } +}; + +const format = Prompt.prototype.format; + +export function template(data: TemplateOptions): string { + const { ctx, message } = data; + + const title = [ + color.gray(S.BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.gray(S.BAR), + }, + }), + ].join('\n'); + + const placeholder = data.placeholder + ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) + : color.inverse(color.hidden('_')); + + const value = data.value ?? ''; + + switch (ctx.state) { + case 'cancel': + return [ + title, + format(value, { + default: { + start: color.gray(S.BAR), + style: (line) => color.strikethrough(color.dim(line)), + }, + }), + ].join('\n'); + + case 'error': + return [ + title, + format(value, { + default: { + start: color.yellow(S.BAR), + }, + }), + data.error ?? + format(ctx.error, { + default: { + start: color.yellow(S.BAR), + style: color.yellow, + }, + lastLine: { + start: color.yellow(S.BAR_END), + }, + }), + ].join('\n'); + + case 'submit': + return [ + title, + format(value, { + default: { + start: color.gray(S.BAR), + style: color.dim, + }, + }), + ].join('\n'); + + default: + return [ + color.gray(S.BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.cyan(S.BAR), + }, + }), + format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { + default: { + start: color.cyan(S.BAR), + }, + }), + color.cyan(S.BAR_END), + ].join('\n'); + } +} diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts new file mode 100644 index 00000000..cce227a3 --- /dev/null +++ b/packages/core/src/themes/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * as S from './symbols'; +export { template as defaultTheme } from './default'; diff --git a/packages/core/src/themes/symbols.ts b/packages/core/src/themes/symbols.ts new file mode 100644 index 00000000..599748ab --- /dev/null +++ b/packages/core/src/themes/symbols.ts @@ -0,0 +1,30 @@ +import isUnicodeSupported from 'is-unicode-supported'; + +const unicode = isUnicodeSupported(); +const s = (c: string, fallback: string) => (unicode ? c : fallback); + +export const STEP_ACTIVE = s('◆', '*'); +export const STEP_CANCEL = s('■', 'x'); +export const STEP_ERROR = s('▲', 'x'); +export const STEP_SUBMIT = s('◇', 'o'); + +export const BAR_START = s('┌', 'T'); +export const BAR = s('│', '|'); +export const BAR_END = s('└', '—'); + +export const RADIO_ACTIVE = s('●', '>'); +export const RADIO_INACTIVE = s('○', ' '); +export const CHECKBOX_ACTIVE = s('◻', '[•]'); +export const CHECKBOX_SELECTED = s('◼', '[+]'); +export const CHECKBOX_INACTIVE = s('◻', '[ ]'); +export const PASSWORD_MASK = s('▪', '•'); + +export const BAR_H = s('─', '-'); +export const CORNER_TOP_RIGHT = s('╮', '+'); +export const CONNECT_LEFT = s('├', '+'); +export const CORNER_BOTTOM_RIGHT = s('╯', '+'); + +export const INFO = s('●', '•'); +export const SUCCESS = s('◆', '*'); +export const WARN = s('▲', '!'); +export const ERROR = s('■', 'x'); diff --git a/packages/core/src/themes/types.ts b/packages/core/src/themes/types.ts new file mode 100644 index 00000000..a3ee8653 --- /dev/null +++ b/packages/core/src/themes/types.ts @@ -0,0 +1,10 @@ +import Prompt from '../prompts/prompt'; + +export interface TemplateOptions { + ctx: Omit; + message: string; + value: string; + valueWithCursor: string | undefined; + placeholder?: string | undefined; + error?: string | undefined; +} From 6a6b719ebc8e56a4b3c18272ab249bc672413a85 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:30:57 -0300 Subject: [PATCH 07/20] feat: theme package --- packages/core/build.config.ts | 11 ++++++++++- packages/core/package.json | 13 +++++++++++++ packages/core/src/index.ts | 10 ---------- packages/core/src/prompts/index.ts | 10 ++++++++++ 4 files changed, 33 insertions(+), 11 deletions(-) delete mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/prompts/index.ts diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index 8bd1ad27..a237314e 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -3,5 +3,14 @@ import { defineBuildConfig } from 'unbuild'; // @see https://github.com/unjs/unbuild export default defineBuildConfig({ preset: '../../build.preset', - entries: ['src/index'], + entries: [ + { + name: 'index', + input: 'src/prompts/index', + }, + { + name: 'themes', + input: 'src/themes/index', + }, + ], }); diff --git a/packages/core/package.json b/packages/core/package.json index 753e30ba..29f36bb8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,9 +10,21 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, + "./themes": { + "types": "./dist/themes.d.ts", + "import": "./dist/themes.mjs", + "require": "./dist/themes.cjs" + }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "themes": [ + "./dist/themes.d.ts" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/natemoo-re/clack", @@ -51,6 +63,7 @@ "test": "vitest run" }, "dependencies": { + "is-unicode-supported": "^1.3.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 25125f99..00000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { default as ConfirmPrompt } from './prompts/confirm'; -export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect'; -export { default as MultiSelectPrompt } from './prompts/multi-select'; -export { default as PasswordPrompt } from './prompts/password'; -export { default as Prompt } from './prompts/prompt'; -export { default as SelectPrompt } from './prompts/select'; -export { default as SelectKeyPrompt } from './prompts/select-key'; -export { default as TextPrompt } from './prompts/text'; -export type { ClackState as State } from './types'; -export { block, isCancel, setGlobalAliases } from './utils'; diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts new file mode 100644 index 00000000..9e73a763 --- /dev/null +++ b/packages/core/src/prompts/index.ts @@ -0,0 +1,10 @@ +export { default as ConfirmPrompt } from './confirm'; +export { default as GroupMultiSelectPrompt } from './group-multiselect'; +export { default as MultiSelectPrompt } from './multi-select'; +export { default as PasswordPrompt } from './password'; +export { default as Prompt, isCancel } from './prompt'; +export type { State } from './prompt'; +export { default as SelectPrompt } from './select'; +export { default as SelectKeyPrompt } from './select-key'; +export { default as TextPrompt } from './text'; +export { block } from '../utils'; From 7452c09f681a2429e81dbc1ee2f49cbcb56fadfd Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:34:08 -0300 Subject: [PATCH 08/20] refactor: prompts with default theme --- package.json | 2 +- packages/core/src/prompts/prompt.ts | 4 +- packages/core/src/themes/default.ts | 4 +- packages/core/src/themes/types.ts | 2 +- packages/prompts/src/index.ts | 851 +++++++++++++++------------- pnpm-lock.yaml | 3 + 6 files changed, 463 insertions(+), 403 deletions(-) diff --git a/package.json b/package.json index 1666382f..4bd441a2 100644 --- a/package.json +++ b/package.json @@ -25,4 +25,4 @@ "volta": { "node": "20.18.1" } -} +} \ No newline at end of file diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index e1802db3..96b0501d 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -351,7 +351,7 @@ export default class Prompt { currentLine += ` ${word}`; } else if (strLength(word) + emptySlots >= maxWidth) { const splitIndex = maxWidth - strLength(currentLine) - emptySlots - 1; - formattedLines.push(currentLine + ' ' + word.slice(0, splitIndex)); + formattedLines.push(`${currentLine} ${word.slice(0, splitIndex)}`); const chunkLength = maxWidth - emptySlots; let chunk = word.slice(splitIndex); @@ -392,7 +392,7 @@ export default class Prompt { // only format the line without the leading space. const leadingSpaceRegex = /^\s/; const styledLine = leadingSpaceRegex.test(line) - ? ' ' + styleLine(line.slice(1)) + ? ` ${styleLine(line.slice(1))}` : styleLine(line); const fullLine = styledLine + ' '.repeat(Math.max(minWidth - strLength(styledLine) - emptySlots, 0)); diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts index f8529218..7d17f1ed 100644 --- a/packages/core/src/themes/default.ts +++ b/packages/core/src/themes/default.ts @@ -1,5 +1,5 @@ -import Prompt, { State } from '../prompts/prompt'; -import { TemplateOptions } from './types'; +import Prompt, { type State } from '../prompts/prompt'; +import type { TemplateOptions } from './types'; import * as S from './symbols'; import color from 'picocolors'; diff --git a/packages/core/src/themes/types.ts b/packages/core/src/themes/types.ts index a3ee8653..14313ae5 100644 --- a/packages/core/src/themes/types.ts +++ b/packages/core/src/themes/types.ts @@ -1,4 +1,4 @@ -import Prompt from '../prompts/prompt'; +import type Prompt from '../prompts/prompt'; export interface TemplateOptions { ctx: Omit; diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 9b611e04..cc559bc5 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -3,60 +3,23 @@ import { GroupMultiSelectPrompt, MultiSelectPrompt, PasswordPrompt, + Prompt, SelectKeyPrompt, SelectPrompt, type State, TextPrompt, block, isCancel, -} from '@clack/core'; -import isUnicodeSupported from 'is-unicode-supported'; -import color from 'picocolors'; -import { cursor, erase } from 'sisteransi'; +} from "@clack/core"; +import { defaultTheme, S } from "@clack/core/themes"; +import isUnicodeSupported from "is-unicode-supported"; +import color from "picocolors"; +import { cursor, erase } from "sisteransi"; -export { isCancel, setGlobalAliases } from '@clack/core'; +export { isCancel, setGlobalAliases } from "@clack/core"; const unicode = isUnicodeSupported(); -const s = (c: string, fallback: string) => (unicode ? c : fallback); -const S_STEP_ACTIVE = s('◆', '*'); -const S_STEP_CANCEL = s('■', 'x'); -const S_STEP_ERROR = s('▲', 'x'); -const S_STEP_SUBMIT = s('◇', 'o'); - -const S_BAR_START = s('┌', 'T'); -const S_BAR = s('│', '|'); -const S_BAR_END = s('└', '—'); - -const S_RADIO_ACTIVE = s('●', '>'); -const S_RADIO_INACTIVE = s('○', ' '); -const S_CHECKBOX_ACTIVE = s('◻', '[•]'); -const S_CHECKBOX_SELECTED = s('◼', '[+]'); -const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); -const S_PASSWORD_MASK = s('▪', '•'); - -const S_BAR_H = s('─', '-'); -const S_CORNER_TOP_RIGHT = s('╮', '+'); -const S_CONNECT_LEFT = s('├', '+'); -const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); - -const S_INFO = s('●', '•'); -const S_SUCCESS = s('◆', '*'); -const S_WARN = s('▲', '!'); -const S_ERROR = s('■', 'x'); - -const symbol = (state: State) => { - switch (state) { - case 'initial': - case 'active': - return color.cyan(S_STEP_ACTIVE); - case 'cancel': - return color.red(S_STEP_CANCEL); - case 'error': - return color.yellow(S_STEP_ERROR); - case 'submit': - return color.green(S_STEP_SUBMIT); - } -}; +const format = Prompt.prototype.format; interface LimitOptionsParams { options: TOption[]; @@ -65,7 +28,9 @@ interface LimitOptionsParams { style: (option: TOption, active: boolean) => string; } -const limitOptions = (params: LimitOptionsParams): string[] => { +const limitOptions = ( + params: LimitOptionsParams +): string[] => { const { cursor, options, style } = params; const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; @@ -75,14 +40,19 @@ const limitOptions = (params: LimitOptionsParams): string[] => let slidingWindowLocation = 0; if (cursor >= slidingWindowLocation + maxItems - 3) { - slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); + slidingWindowLocation = Math.max( + Math.min(cursor - maxItems + 3, options.length - maxItems), + 0 + ); } else if (cursor < slidingWindowLocation + 2) { slidingWindowLocation = Math.max(cursor - 2, 0); } - const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; + const shouldRenderTopEllipsis = + maxItems < options.length && slidingWindowLocation > 0; const shouldRenderBottomEllipsis = - maxItems < options.length && slidingWindowLocation + maxItems < options.length; + maxItems < options.length && + slidingWindowLocation + maxItems < options.length; return options .slice(slidingWindowLocation, slidingWindowLocation + maxItems) @@ -90,7 +60,7 @@ const limitOptions = (params: LimitOptionsParams): string[] => const isTopLimit = i === 0 && shouldRenderTopEllipsis; const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; return isTopLimit || isBottomLimit - ? color.dim('...') + ? color.dim("...") : style(option, i + slidingWindowLocation === cursor); }); }; @@ -109,80 +79,13 @@ export const text = (opts: TextOptions) => { defaultValue: opts.defaultValue, initialValue: opts.initialValue, render() { - const title = [ - color.gray(S_BAR), - this.format(opts.message, { - firstLine: { - start: symbol(this.state), - }, - default: { - start: color.gray(S_BAR), - }, - }), - ].join('\n'); - const placeholder = opts.placeholder - ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) - : color.inverse(color.hidden('_')); - const value = !this.value ? placeholder : this.valueWithCursor; - - switch (this.state) { - case 'error': - return [ - title, - this.format(value, { - default: { - start: color.yellow(S_BAR), - }, - }), - this.format(this.error, { - default: { - start: color.yellow(S_BAR), - style: color.yellow, - }, - lastLine: { - start: color.yellow(S_BAR_END), - }, - }), - ].join('\n'); - case 'submit': - return [ - title, - this.format(this.value ?? opts.placeholder, { - default: { - start: color.gray(S_BAR), - style: color.dim, - }, - }), - ].join('\n'); - case 'cancel': - return [ - title, - this.format(this.value ?? '', { - default: { - start: color.gray(S_BAR), - style: (line) => color.strikethrough(color.dim(line)), - }, - }), - ].join('\n'); - default: - return [ - color.gray(S_BAR), - this.format(opts.message, { - firstLine: { - start: symbol(this.state), - }, - default: { - start: color.cyan(S_BAR), - }, - }), - this.format(value, { - default: { - start: color.cyan(S_BAR), - }, - }), - color.cyan(S_BAR_END), - ].join('\n'); - } + return defaultTheme({ + ctx: this, + message: opts.message, + value: this.value, + valueWithCursor: this.valueWithCursor, + placeholder: opts.placeholder, + }); }, }).prompt() as Promise; }; @@ -195,26 +98,14 @@ export interface PasswordOptions { export const password = (opts: PasswordOptions) => { return new PasswordPrompt({ validate: opts.validate, - mask: opts.mask ?? S_PASSWORD_MASK, + mask: opts.mask ?? S.PASSWORD_MASK, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const value = this.valueWithCursor; - const masked = this.masked; - - switch (this.state) { - case 'error': - return `${title.trim()}\n${color.yellow(S_BAR)} ${masked}\n${color.yellow( - S_BAR_END - )} ${color.yellow(this.error)}\n`; - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(masked)}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(masked ?? ''))}${ - masked ? `\n${color.gray(S_BAR)}` : '' - }`; - default: - return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; - } + return defaultTheme({ + ctx: this, + message: opts.message, + value: this.value, + valueWithCursor: this.valueWithCursor, + }); }, }).prompt() as Promise; }; @@ -226,35 +117,32 @@ export interface ConfirmOptions { initialValue?: boolean; } export const confirm = (opts: ConfirmOptions) => { - const active = opts.active ?? 'Yes'; - const inactive = opts.inactive ?? 'No'; + const active = opts.active ?? "Yes"; + const inactive = opts.inactive ?? "No"; return new ConfirmPrompt({ active, inactive, initialValue: opts.initialValue ?? true, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - const value = this.value ? active : inactive; - - switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${color.strikethrough( - color.dim(value) - )}\n${color.gray(S_BAR)}`; - default: { - return `${title}${color.cyan(S_BAR)} ${ - this.value - ? `${color.green(S_RADIO_ACTIVE)} ${active}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` - } ${color.dim('/')} ${ - !this.value - ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` - }\n${color.cyan(S_BAR_END)}\n`; - } - } + const opt = (state: boolean, message: string): string => { + return state + ? `${color.green(S.RADIO_ACTIVE)} ${message}` + : `${color.dim(S.RADIO_INACTIVE)} ${color.dim(message)}`; + }; + return defaultTheme({ + ctx: this, + message: opts.message, + value: + this.state === "submit" || this.state === "cancel" + ? this.value + ? active + : inactive + : `${opt(!!this.value, active)} ${color.dim("/")} ${opt( + !this.value, + inactive + )}`, + valueWithCursor: undefined, + }); }, }).prompt() as Promise; }; @@ -273,19 +161,22 @@ export interface SelectOptions { } export const select = (opts: SelectOptions) => { - const opt = (option: Option, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { + const opt = ( + option: Option, + state: "inactive" | "active" | "selected" | "cancelled" + ) => { const label = option.label ?? String(option.value); switch (state) { - case 'selected': + case "selected": return `${color.dim(label)}`; - case 'active': - return `${color.green(S_RADIO_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + case "active": + return `${color.green(S.RADIO_ACTIVE)} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : "" }`; - case 'cancelled': + case "cancelled": return `${color.strikethrough(color.dim(label))}`; default: - return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S.RADIO_INACTIVE)} ${color.dim(label)}`; } }; @@ -293,25 +184,30 @@ export const select = (opts: SelectOptions) => { options: opts.options, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; - + let value: string; switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt( - this.options[this.cursor], - 'cancelled' - )}\n${color.gray(S_BAR)}`; + case "submit": + value = opt(this.options[this.cursor], "selected"); + break; + case "cancel": + value = opt(this.options[this.cursor], "cancelled"); + break; default: { - return `${title}${color.cyan(S_BAR)} ${limitOptions({ + value = limitOptions({ cursor: this.cursor, options: this.options, maxItems: opts.maxItems, - style: (item, active) => opt(item, active ? 'active' : 'inactive'), - }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + style: (item, active) => opt(item, active ? "active" : "inactive"), + }).join("\n"); + break; } } + return defaultTheme({ + ctx: this, + message: opts.message, + value, + valueWithCursor: undefined, + }); }, }).prompt() as Promise; }; @@ -319,47 +215,57 @@ export const select = (opts: SelectOptions) => { export const selectKey = (opts: SelectOptions) => { const opt = ( option: Option, - state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' + state: "inactive" | "active" | "selected" | "cancelled" = "inactive" ) => { const label = option.label ?? String(option.value); - if (state === 'selected') { + if (state === "selected") { return `${color.dim(label)}`; } - if (state === 'cancelled') { + if (state === "cancelled") { return `${color.strikethrough(color.dim(label))}`; } - if (state === 'active') { + if (state === "active") { return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + option.hint ? color.dim(`(${option.hint})`) : "" }`; } - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; + return `${color.gray( + color.bgWhite(color.inverse(` ${option.value} `)) + )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ""}`; }; return new SelectKeyPrompt({ options: opts.options, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + let value: string; switch (this.state) { - case 'submit': - return `${title}${color.gray(S_BAR)} ${opt( - this.options.find((opt) => opt.value === this.value) ?? opts.options[0], - 'selected' - )}`; - case 'cancel': - return `${title}${color.gray(S_BAR)} ${opt(this.options[0], 'cancelled')}\n${color.gray( - S_BAR - )}`; - default: { - return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; - } + case "submit": + value = opt( + // biome-ignore lint/style/noNonNullAssertion: + this.options.find((opt) => opt.value === this.value)!, + "selected" + ); + break; + case "cancel": + value = opt(this.options[0], "cancelled"); + break; + default: + value = this.options + .map((option, i) => + opt(option, i === this.cursor ? "active" : "inactive") + ) + .join("\n"); + break; } + + return defaultTheme({ + ctx: this, + message: opts.message, + value, + valueWithCursor: undefined, + }); }, }).prompt() as Promise; }; @@ -375,29 +281,35 @@ export interface MultiSelectOptions { export const multiselect = (opts: MultiSelectOptions) => { const opt = ( option: Option, - state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' + state: + | "inactive" + | "active" + | "selected" + | "active-selected" + | "submitted" + | "cancelled" ) => { const label = option.label ?? String(option.value); - if (state === 'active') { - return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + if (state === "active") { + return `${color.cyan(S.CHECKBOX_ACTIVE)} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : "" }`; } - if (state === 'selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + if (state === "selected") { + return `${color.green(S.CHECKBOX_SELECTED)} ${color.dim(label)}`; } - if (state === 'cancelled') { + if (state === "cancelled") { return `${color.strikethrough(color.dim(label))}`; } - if (state === 'active-selected') { - return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + if (state === "active-selected") { + return `${color.green(S.CHECKBOX_SELECTED)} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : "" }`; } - if (state === 'submitted') { + if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim(label)}`; }; return new MultiSelectPrompt({ @@ -409,67 +321,86 @@ export const multiselect = (opts: MultiSelectOptions) => { if (this.required && selected.length === 0) return `Please select at least one option.\n${color.reset( color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) + `Press ${color.gray( + color.bgWhite(color.inverse(" space ")) + )} to select, ${color.gray( + color.bgWhite(color.inverse(" enter ")) )} to submit` ) )}`; }, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + let value: string; + let error: string | undefined; const styleOption = (option: Option, active: boolean) => { const selected = this.value.includes(option.value); if (active && selected) { - return opt(option, 'active-selected'); + return opt(option, "active-selected"); } if (selected) { - return opt(option, 'selected'); + return opt(option, "selected"); } - return opt(option, active ? 'active' : 'inactive'); + return opt(option, active ? "active" : "inactive"); }; switch (this.state) { - case 'submit': { - return `${title}${color.gray(S_BAR)} ${ + case "submit": { + value = this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'submitted')) - .join(color.dim(', ')) || color.dim('none') - }`; + .map((option) => opt(option, "submitted")) + .join(color.dim(", ")) || color.dim("none"); + break; } - case 'cancel': { - const label = this.options - .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' - }`; + case "cancel": { + value = + this.options + .filter(({ value }) => this.value.includes(value)) + .map((option) => opt(option, "cancelled")) + .join(color.dim(", ")) ?? ""; + break; } - case 'error': { - const footer = this.error - .split('\n') - .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` - ) - .join('\n'); - return `${title + color.yellow(S_BAR)} ${limitOptions({ - options: this.options, + case "error": { + error = format( + this.error + .split("\n") + .map((ln, i) => (i === 0 ? color.yellow(ln) : ln)) + .join("\n"), + { + firstLine: { + start: color.yellow(S.BAR_END), + }, + default: { + start: color.hidden("-"), + }, + } + ); + value = limitOptions({ cursor: this.cursor, maxItems: opts.maxItems, + options: this.options, style: styleOption, - }).join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + }).join("\n"); + break; } default: { - return `${title}${color.cyan(S_BAR)} ${limitOptions({ - options: this.options, + value = limitOptions({ cursor: this.cursor, maxItems: opts.maxItems, + options: this.options, style: styleOption, - }).join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + }).join("\n"); + break; } } + return defaultTheme({ + ctx: this, + message: opts.message, + value, + error, + valueWithCursor: undefined, + }); }, }).prompt() as Promise; }; @@ -481,52 +412,59 @@ export interface GroupMultiSelectOptions { required?: boolean; cursorAt?: Value; } -export const groupMultiselect = (opts: GroupMultiSelectOptions) => { +export const groupMultiselect = ( + opts: GroupMultiSelectOptions +) => { const opt = ( option: Option, state: - | 'inactive' - | 'active' - | 'selected' - | 'active-selected' - | 'group-active' - | 'group-active-selected' - | 'submitted' - | 'cancelled', + | "inactive" + | "active" + | "selected" + | "active-selected" + | "group-active" + | "group-active-selected" + | "submitted" + | "cancelled", options: Option[] = [] ) => { const label = option.label ?? String(option.value); - const isItem = typeof (option as any).group === 'string'; - const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); + const isItem = typeof (option as any).group === "string"; + const next = + isItem && (options[options.indexOf(option) + 1] ?? { group: true }); const isLast = isItem && (next as any).group === true; - const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''; + const prefix = isItem ? `${isLast ? S.BAR_END : S.BAR} ` : ""; - if (state === 'active') { - return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' + if (state === "active") { + return `${color.dim(prefix)}${color.cyan(S.CHECKBOX_ACTIVE)} ${label} ${ + option.hint ? color.dim(`(${option.hint})`) : "" }`; } - if (state === 'group-active') { - return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; + if (state === "group-active") { + return `${prefix}${color.cyan(S.CHECKBOX_ACTIVE)} ${color.dim(label)}`; } - if (state === 'group-active-selected') { - return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + if (state === "group-active-selected") { + return `${prefix}${color.green(S.CHECKBOX_SELECTED)} ${color.dim(label)}`; } - if (state === 'selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; + if (state === "selected") { + return `${color.dim(prefix)}${color.green( + S.CHECKBOX_SELECTED + )} ${color.dim(label)}`; } - if (state === 'cancelled') { + if (state === "cancelled") { return `${color.strikethrough(color.dim(label))}`; } - if (state === 'active-selected') { - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : '' - }`; + if (state === "active-selected") { + return `${color.dim(prefix)}${color.green( + S.CHECKBOX_SELECTED + )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ""}`; } - if (state === 'submitted') { + if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(prefix)}${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim( + label + )}`; }; return new GroupMultiSelectPrompt({ @@ -538,175 +476,295 @@ export const groupMultiselect = (opts: GroupMultiSelectOptions) => if (this.required && selected.length === 0) return `Please select at least one option.\n${color.reset( color.dim( - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( - color.bgWhite(color.inverse(' enter ')) + `Press ${color.gray( + color.bgWhite(color.inverse(" space ")) + )} to select, ${color.gray( + color.bgWhite(color.inverse(" enter ")) )} to submit` ) )}`; }, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; + const symbol = (state: State) => { + switch (state) { + case "initial": + case "active": + return color.cyan(S.STEP_ACTIVE); + case "cancel": + return color.red(S.STEP_CANCEL); + case "error": + return color.yellow(S.STEP_ERROR); + case "submit": + return color.green(S.STEP_SUBMIT); + } + }; + + const title = `${color.gray(S.BAR)}\n${symbol(this.state)} ${ + opts.message + }\n`; switch (this.state) { - case 'submit': { - return `${title}${color.gray(S_BAR)} ${this.options + case "submit": { + return `${title}\n${color.gray(S.BAR)} ${this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'submitted')) - .join(color.dim(', '))}`; + .map((option) => opt(option, "submitted")) + .join(color.dim(", "))}`; } - case 'cancel': { + case "cancel": { const label = this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, 'cancelled')) - .join(color.dim(', ')); - return `${title}${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' + .map((option) => opt(option, "cancelled")) + .join(color.dim(", ")); + return `${title}\n${color.gray(S.BAR)} ${ + label.trim() ? `${label}\n${color.gray(S.BAR)}` : "" }`; } - case 'error': { + case "error": { const footer = this.error - .split('\n') + .split("\n") .map((ln, i) => - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` + i === 0 + ? `${color.yellow(S.BAR_END)} ${color.yellow(ln)}` + : ` ${ln}` ) - .join('\n'); - return `${title}${color.yellow(S_BAR)} ${this.options + .join("\n"); + return `${title}\n${color.yellow(S.BAR)} ${this.options .map((option, i, options) => { const selected = this.value.includes(option.value) || - (option.group === true && this.isGroupSelected(`${option.value}`)); + (option.group === true && + this.isGroupSelected(`${option.value}`)); const active = i === this.cursor; const groupActive = !active && - typeof option.group === 'string' && + typeof option.group === "string" && this.options[this.cursor].value === option.group; if (groupActive) { - return opt(option, selected ? 'group-active-selected' : 'group-active', options); + return opt( + option, + selected ? "group-active-selected" : "group-active", + options + ); } if (active && selected) { - return opt(option, 'active-selected', options); + return opt(option, "active-selected", options); } if (selected) { - return opt(option, 'selected', options); + return opt(option, "selected", options); } - return opt(option, active ? 'active' : 'inactive', options); + return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + .join(`\n${color.yellow(S.BAR)} `)}\n${footer}\n`; } default: { - return `${title}${color.cyan(S_BAR)} ${this.options + return `${title}\n${color.cyan(S.BAR)} ${this.options .map((option, i, options) => { const selected = this.value.includes(option.value) || - (option.group === true && this.isGroupSelected(`${option.value}`)); + (option.group === true && + this.isGroupSelected(`${option.value}`)); const active = i === this.cursor; const groupActive = !active && - typeof option.group === 'string' && + typeof option.group === "string" && this.options[this.cursor].value === option.group; if (groupActive) { - return opt(option, selected ? 'group-active-selected' : 'group-active', options); + return opt( + option, + selected ? "group-active-selected" : "group-active", + options + ); } if (active && selected) { - return opt(option, 'active-selected', options); + return opt(option, "active-selected", options); } if (selected) { - return opt(option, 'selected', options); + return opt(option, "selected", options); } - return opt(option, active ? 'active' : 'inactive', options); + return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; } } }, }).prompt() as Promise; }; -const strip = (str: string) => str.replace(ansiRegex(), ''); -export const note = (message = '', title = '') => { - const lines = `\n${message}\n`.split('\n'); - const titleLen = strip(title).length; - const len = - Math.max( - lines.reduce((sum, ln) => { - const line = strip(ln); - return line.length > sum ? line.length : sum; - }, 0), - titleLen - ) + 2; - const msg = lines - .map( - (ln) => - `${color.gray(S_BAR)} ${color.dim(ln)}${' '.repeat(len - strip(ln).length)}${color.gray( - S_BAR - )}` - ) - .join('\n'); - process.stdout.write( - `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( - S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT - )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` - ); +const strip = (str: string) => str.replace(ansiRegex(), ""); +const strLength = (str: string) => { + if (!str) return 0; + + const colorCodeRegex = /\x1B\[[0-9;]*[mG]/g; + const arr = [...strip(str.replace(colorCodeRegex, ""))]; + let len = 0; + + for (const char of arr) { + len += char.charCodeAt(0) > 127 || char.charCodeAt(0) === 94 ? 2 : 1; + } + return len; +}; +export const note = (message = "", title = "") => { + const maxWidth = Math.floor((process.stdout.columns ?? 80) * 0.8); + const lines = format(message, { + default: { + start: color.gray(S.BAR), + }, + maxWidth: maxWidth - 2, + }).split(/\n/g); + const titleLen = strLength(title); + const messageLen = lines.reduce((sum, line) => { + const length = strLength(line); + return length > sum ? length : sum; + }, 0); + const len = Math.min(Math.max(messageLen, titleLen) + 2, maxWidth); + const noteBox = [ + color.gray(S.BAR), + `${color.green(S.STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S.BAR_H.repeat(Math.max(len - titleLen - 3, 0)) + S.CORNER_TOP_RIGHT + )}`, + color.gray(S.BAR + " ".repeat(len) + S.BAR), + lines + .map( + (line) => + line + + " ".repeat(Math.max(len + (unicode ? 2 : 1) - strLength(line), 0)) + + color.gray(S.BAR) + ) + .join("\n"), + color.gray(S.BAR + " ".repeat(len) + S.BAR), + color.gray(S.CONNECT_LEFT + S.BAR_H.repeat(len) + S.CORNER_BOTTOM_RIGHT), + "", + ].join("\n"); + process.stdout.write(noteBox); }; -export const cancel = (message = '') => { - process.stdout.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); +export const cancel = (message = "") => { + process.stdout.write( + `${format(message, { + default: { + start: color.gray(S.BAR), + style: color.red, + }, + lastLine: { + start: color.gray(S.BAR_END), + }, + })}\n\n` + ); }; -export const intro = (title = '') => { - process.stdout.write(`${color.gray(S_BAR_START)} ${title}\n`); +export const intro = (title = "") => { + process.stdout.write( + `${format(title, { + firstLine: { + start: color.gray(S.BAR_START), + }, + default: { + start: color.gray(S.BAR), + }, + })}\n` + ); }; -export const outro = (message = '') => { - process.stdout.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`); +export const outro = (message = "") => { + process.stdout.write( + [ + color.gray(S.BAR), + format(message, { + default: { + start: color.gray(S.BAR), + }, + lastLine: { + start: color.gray(S.BAR_END), + }, + }), + "", + "", + ].join("\n") + ); }; export type LogMessageOptions = { symbol?: string; }; export const log = { - message: (message = '', { symbol = color.gray(S_BAR) }: LogMessageOptions = {}) => { - const parts = [`${color.gray(S_BAR)}`]; - if (message) { - const [firstLine, ...lines] = message.split('\n'); - parts.push(`${symbol} ${firstLine}`, ...lines.map((ln) => `${color.gray(S_BAR)} ${ln}`)); - } - process.stdout.write(`${parts.join('\n')}\n`); + message: ( + message = "", + { symbol = color.gray(S.BAR) }: LogMessageOptions = {} + ) => { + process.stdout.write( + `${format(message, { + firstLine: { + start: symbol, + }, + default: { + start: color.gray(S.BAR), + }, + })}\n` + ); }, info: (message: string) => { - log.message(message, { symbol: color.blue(S_INFO) }); + log.message(message, { + symbol: color.blue(S.INFO), + }); }, success: (message: string) => { - log.message(message, { symbol: color.green(S_SUCCESS) }); + log.message(message, { + symbol: color.green(S.SUCCESS), + }); }, step: (message: string) => { - log.message(message, { symbol: color.green(S_STEP_SUBMIT) }); + log.message(message, { + symbol: color.green(S.STEP_SUBMIT), + }); }, warn: (message: string) => { - log.message(message, { symbol: color.yellow(S_WARN) }); + log.message(message, { + symbol: color.yellow(S.WARN), + }); }, /** alias for `log.warn()`. */ warning: (message: string) => { log.warn(message); }, error: (message: string) => { - log.message(message, { symbol: color.red(S_ERROR) }); + log.message(message, { + symbol: color.red(S.ERROR), + }); }, }; export const spinner = () => { - const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; + const frames = unicode ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"]; const delay = unicode ? 80 : 120; - const isCI = process.env.CI === 'true'; + const isCI = process.env.CI === "true"; let unblock: () => void; let loop: NodeJS.Timeout; let isSpinnerActive = false; - let _message = ''; + let _message = ""; let _prevMessage: string | undefined = undefined; + const formatMessage = (symbol: string, msg: string): string => { + return format(msg, { + firstLine: { + start: symbol, + }, + default: { + start: color.gray(S.BAR), + }, + }); + }; + + const clearPrevMessage = (): void => { + if (_prevMessage === undefined) return; + if (isCI) process.stdout.write("\n"); + const linesCounter = _prevMessage.split(/\n/g).length; + process.stdout.write(cursor.move(-999, (linesCounter - 1) * -1)); + process.stdout.write(erase.down(linesCounter)); + }; + const handleExit = (code: number) => { - const msg = code > 1 ? 'Something went wrong' : 'Canceled'; + const msg = code > 1 ? "Something went wrong" : "Canceled"; if (isSpinnerActive) stop(msg, code); }; @@ -715,40 +773,32 @@ export const spinner = () => { const registerHooks = () => { // Reference: https://nodejs.org/api/process.html#event-uncaughtexception - process.on('uncaughtExceptionMonitor', errorEventHandler); + process.on("uncaughtExceptionMonitor", errorEventHandler); // Reference: https://nodejs.org/api/process.html#event-unhandledrejection - process.on('unhandledRejection', errorEventHandler); + process.on("unhandledRejection", errorEventHandler); // Reference Signal Events: https://nodejs.org/api/process.html#signal-events - process.on('SIGINT', signalEventHandler); - process.on('SIGTERM', signalEventHandler); - process.on('exit', handleExit); + process.on("SIGINT", signalEventHandler); + process.on("SIGTERM", signalEventHandler); + process.on("exit", handleExit); }; const clearHooks = () => { - process.removeListener('uncaughtExceptionMonitor', errorEventHandler); - process.removeListener('unhandledRejection', errorEventHandler); - process.removeListener('SIGINT', signalEventHandler); - process.removeListener('SIGTERM', signalEventHandler); - process.removeListener('exit', handleExit); - }; - - const clearPrevMessage = () => { - if (_prevMessage === undefined) return; - if (isCI) process.stdout.write('\n'); - const prevLines = _prevMessage.split('\n'); - process.stdout.write(cursor.move(-999, prevLines.length - 1)); - process.stdout.write(erase.down(prevLines.length)); + process.removeListener("uncaughtExceptionMonitor", errorEventHandler); + process.removeListener("unhandledRejection", errorEventHandler); + process.removeListener("SIGINT", signalEventHandler); + process.removeListener("SIGTERM", signalEventHandler); + process.removeListener("exit", handleExit); }; const parseMessage = (msg: string): string => { - return msg.replace(/\.+$/, ''); + return msg.replace(/\.+$/, ""); }; - const start = (msg = ''): void => { + const start = (msg = ""): void => { isSpinnerActive = true; unblock = block(); _message = parseMessage(msg); - process.stdout.write(`${color.gray(S_BAR)}\n`); + process.stdout.write(`${color.gray(S.BAR)}\n`); let frameIndex = 0; let dotsTimer = 0; registerHooks(); @@ -759,31 +809,34 @@ export const spinner = () => { clearPrevMessage(); _prevMessage = _message; const frame = color.magenta(frames[frameIndex]); - const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); - process.stdout.write(`${frame} ${_message}${loadingDots}`); + const loadingDots = isCI + ? "..." + : ".".repeat(Math.floor(dotsTimer)).slice(0, 3); + _prevMessage = _message; + process.stdout.write(formatMessage(frame, _message + loadingDots)); frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0; }, delay); }; - const stop = (msg = '', code = 0): void => { + const stop = (msg = "", code = 0): void => { isSpinnerActive = false; clearInterval(loop); clearPrevMessage(); const step = code === 0 - ? color.green(S_STEP_SUBMIT) + ? color.green(S.STEP_SUBMIT) : code === 1 - ? color.red(S_STEP_CANCEL) - : color.red(S_STEP_ERROR); + ? color.red(S.STEP_CANCEL) + : color.red(S.STEP_ERROR); _message = parseMessage(msg ?? _message); process.stdout.write(`${step} ${_message}\n`); clearHooks(); unblock(); }; - const message = (msg = ''): void => { - _message = parseMessage(msg ?? _message); + const message = (msg = ""): void => { + _message = parseMessage(msg || _message); }; return { @@ -797,11 +850,11 @@ export const spinner = () => { // @see LICENSE function ansiRegex() { const pattern = [ - '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', - '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', - ].join('|'); + "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", + "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", + ].join("|"); - return new RegExp(pattern, 'g'); + return new RegExp(pattern, "g"); } export type PromptGroupAwaitedReturn = { @@ -813,7 +866,9 @@ export interface PromptGroupOptions { * Control how the group can be canceled * if one of the prompts is canceled. */ - onCancel?: (opts: { results: Prettify>> }) => void; + onCancel?: (opts: { + results: Prettify>>; + }) => void; } type Prettify = { @@ -846,8 +901,8 @@ export const group = async ( // Pass the results to the onCancel function // so the user can decide what to do with the results // TODO: Switch to callback within core to avoid isCancel Fn - if (typeof opts?.onCancel === 'function' && isCancel(result)) { - results[name] = 'canceled'; + if (typeof opts?.onCancel === "function" && isCancel(result)) { + results[name] = "canceled"; opts.onCancel({ results }); continue; } @@ -866,7 +921,9 @@ export type Task = { /** * Task function */ - task: (message: (string: string) => void) => string | Promise | void | Promise; + task: ( + message: (string: string) => void + ) => string | Promise | void | Promise; /** * If enabled === false the task will be skipped diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index da2a1e3c..2a6bf688 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,6 +58,9 @@ importers: packages/core: dependencies: + is-unicode-supported: + specifier: ^1.3.0 + version: 1.3.0 picocolors: specifier: ^1.0.0 version: 1.0.0 From d67f6939d0621e790d698c07730987766af30b40 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:41:33 -0300 Subject: [PATCH 09/20] fix: ci --- packages/core/src/prompts/index.ts | 2 +- packages/core/src/themes/default.ts | 53 ++++++++++++++++------------- packages/core/src/themes/index.ts | 4 +-- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts index 9e73a763..2bd9f645 100644 --- a/packages/core/src/prompts/index.ts +++ b/packages/core/src/prompts/index.ts @@ -1,3 +1,4 @@ +export { block } from '../utils'; export { default as ConfirmPrompt } from './confirm'; export { default as GroupMultiSelectPrompt } from './group-multiselect'; export { default as MultiSelectPrompt } from './multi-select'; @@ -7,4 +8,3 @@ export type { State } from './prompt'; export { default as SelectPrompt } from './select'; export { default as SelectKeyPrompt } from './select-key'; export { default as TextPrompt } from './text'; -export { block } from '../utils'; diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts index 7d17f1ed..582e5f6b 100644 --- a/packages/core/src/themes/default.ts +++ b/packages/core/src/themes/default.ts @@ -1,18 +1,18 @@ -import Prompt, { type State } from '../prompts/prompt'; -import type { TemplateOptions } from './types'; -import * as S from './symbols'; -import color from 'picocolors'; +import color from "picocolors"; +import Prompt, { type State } from "../prompts/prompt"; +import * as S from "./symbols"; +import type { TemplateOptions } from "./types"; const symbol = (state: State) => { switch (state) { - case 'initial': - case 'active': + case "initial": + case "active": return color.cyan(S.STEP_ACTIVE); - case 'cancel': + case "cancel": return color.red(S.STEP_CANCEL); - case 'error': + case "error": return color.yellow(S.STEP_ERROR); - case 'submit': + case "submit": return color.green(S.STEP_SUBMIT); } }; @@ -32,16 +32,16 @@ export function template(data: TemplateOptions): string { start: color.gray(S.BAR), }, }), - ].join('\n'); + ].join("\n"); const placeholder = data.placeholder ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) - : color.inverse(color.hidden('_')); + : color.inverse(color.hidden("_")); - const value = data.value ?? ''; + const value = data.value ?? ""; switch (ctx.state) { - case 'cancel': + case "cancel": return [ title, format(value, { @@ -50,9 +50,9 @@ export function template(data: TemplateOptions): string { style: (line) => color.strikethrough(color.dim(line)), }, }), - ].join('\n'); + ].join("\n"); - case 'error': + case "error": return [ title, format(value, { @@ -70,9 +70,9 @@ export function template(data: TemplateOptions): string { start: color.yellow(S.BAR_END), }, }), - ].join('\n'); + ].join("\n"); - case 'submit': + case "submit": return [ title, format(value, { @@ -81,7 +81,7 @@ export function template(data: TemplateOptions): string { style: color.dim, }, }), - ].join('\n'); + ].join("\n"); default: return [ @@ -94,12 +94,17 @@ export function template(data: TemplateOptions): string { start: color.cyan(S.BAR), }, }), - format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { - default: { - start: color.cyan(S.BAR), - }, - }), + format( + data.placeholder && !data.value + ? placeholder + : data.valueWithCursor ?? value, + { + default: { + start: color.cyan(S.BAR), + }, + } + ), color.cyan(S.BAR_END), - ].join('\n'); + ].join("\n"); } } diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts index cce227a3..d26b9cb9 100644 --- a/packages/core/src/themes/index.ts +++ b/packages/core/src/themes/index.ts @@ -1,3 +1,3 @@ -export * from './types'; -export * as S from './symbols'; export { template as defaultTheme } from './default'; +export * as S from './symbols'; +export * from './types'; From e57c5831e670639d73c8672f1657d1ec0a9b1875 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 21 Aug 2023 12:07:15 -0300 Subject: [PATCH 10/20] refactor: revert themes subpath --- packages/core/build.config.ts | 11 +- packages/core/package.json | 13 -- packages/core/src/index.ts | 10 ++ packages/core/src/prompts/index.ts | 10 -- packages/core/src/themes/default.ts | 110 ------------ packages/core/src/themes/index.ts | 3 - packages/core/src/themes/symbols.ts | 30 ---- packages/core/src/themes/types.ts | 10 -- packages/prompts/src/index.ts | 265 +++++++++++++++++++++------- pnpm-lock.yaml | 23 +-- 10 files changed, 217 insertions(+), 268 deletions(-) create mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/prompts/index.ts delete mode 100644 packages/core/src/themes/default.ts delete mode 100644 packages/core/src/themes/index.ts delete mode 100644 packages/core/src/themes/symbols.ts delete mode 100644 packages/core/src/themes/types.ts diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index a237314e..8bd1ad27 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -3,14 +3,5 @@ import { defineBuildConfig } from 'unbuild'; // @see https://github.com/unjs/unbuild export default defineBuildConfig({ preset: '../../build.preset', - entries: [ - { - name: 'index', - input: 'src/prompts/index', - }, - { - name: 'themes', - input: 'src/themes/index', - }, - ], + entries: ['src/index'], }); diff --git a/packages/core/package.json b/packages/core/package.json index 29f36bb8..753e30ba 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,21 +10,9 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, - "./themes": { - "types": "./dist/themes.d.ts", - "import": "./dist/themes.mjs", - "require": "./dist/themes.cjs" - }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts", - "typesVersions": { - "*": { - "themes": [ - "./dist/themes.d.ts" - ] - } - }, "repository": { "type": "git", "url": "https://github.com/natemoo-re/clack", @@ -63,7 +51,6 @@ "test": "vitest run" }, "dependencies": { - "is-unicode-supported": "^1.3.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..3084aa3b --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,10 @@ +export { default as ConfirmPrompt } from "./prompts/confirm"; +export { default as GroupMultiSelectPrompt } from "./prompts/group-multiselect"; +export { default as MultiSelectPrompt } from "./prompts/multi-select"; +export { default as PasswordPrompt } from "./prompts/password"; +export { default as Prompt } from "./prompts/prompt"; +export type { State } from "./prompts/prompt"; +export { default as SelectPrompt } from "./prompts/select"; +export { default as SelectKeyPrompt } from "./prompts/select-key"; +export { default as TextPrompt } from "./prompts/text"; +export { block, isCancel, setGlobalAliases } from "./utils"; diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts deleted file mode 100644 index 2bd9f645..00000000 --- a/packages/core/src/prompts/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { block } from '../utils'; -export { default as ConfirmPrompt } from './confirm'; -export { default as GroupMultiSelectPrompt } from './group-multiselect'; -export { default as MultiSelectPrompt } from './multi-select'; -export { default as PasswordPrompt } from './password'; -export { default as Prompt, isCancel } from './prompt'; -export type { State } from './prompt'; -export { default as SelectPrompt } from './select'; -export { default as SelectKeyPrompt } from './select-key'; -export { default as TextPrompt } from './text'; diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts deleted file mode 100644 index 582e5f6b..00000000 --- a/packages/core/src/themes/default.ts +++ /dev/null @@ -1,110 +0,0 @@ -import color from "picocolors"; -import Prompt, { type State } from "../prompts/prompt"; -import * as S from "./symbols"; -import type { TemplateOptions } from "./types"; - -const symbol = (state: State) => { - switch (state) { - case "initial": - case "active": - return color.cyan(S.STEP_ACTIVE); - case "cancel": - return color.red(S.STEP_CANCEL); - case "error": - return color.yellow(S.STEP_ERROR); - case "submit": - return color.green(S.STEP_SUBMIT); - } -}; - -const format = Prompt.prototype.format; - -export function template(data: TemplateOptions): string { - const { ctx, message } = data; - - const title = [ - color.gray(S.BAR), - format(message, { - firstLine: { - start: symbol(ctx.state), - }, - default: { - start: color.gray(S.BAR), - }, - }), - ].join("\n"); - - const placeholder = data.placeholder - ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) - : color.inverse(color.hidden("_")); - - const value = data.value ?? ""; - - switch (ctx.state) { - case "cancel": - return [ - title, - format(value, { - default: { - start: color.gray(S.BAR), - style: (line) => color.strikethrough(color.dim(line)), - }, - }), - ].join("\n"); - - case "error": - return [ - title, - format(value, { - default: { - start: color.yellow(S.BAR), - }, - }), - data.error ?? - format(ctx.error, { - default: { - start: color.yellow(S.BAR), - style: color.yellow, - }, - lastLine: { - start: color.yellow(S.BAR_END), - }, - }), - ].join("\n"); - - case "submit": - return [ - title, - format(value, { - default: { - start: color.gray(S.BAR), - style: color.dim, - }, - }), - ].join("\n"); - - default: - return [ - color.gray(S.BAR), - format(message, { - firstLine: { - start: symbol(ctx.state), - }, - default: { - start: color.cyan(S.BAR), - }, - }), - format( - data.placeholder && !data.value - ? placeholder - : data.valueWithCursor ?? value, - { - default: { - start: color.cyan(S.BAR), - }, - } - ), - color.cyan(S.BAR_END), - ].join("\n"); - } -} diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts deleted file mode 100644 index d26b9cb9..00000000 --- a/packages/core/src/themes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { template as defaultTheme } from './default'; -export * as S from './symbols'; -export * from './types'; diff --git a/packages/core/src/themes/symbols.ts b/packages/core/src/themes/symbols.ts deleted file mode 100644 index 599748ab..00000000 --- a/packages/core/src/themes/symbols.ts +++ /dev/null @@ -1,30 +0,0 @@ -import isUnicodeSupported from 'is-unicode-supported'; - -const unicode = isUnicodeSupported(); -const s = (c: string, fallback: string) => (unicode ? c : fallback); - -export const STEP_ACTIVE = s('◆', '*'); -export const STEP_CANCEL = s('■', 'x'); -export const STEP_ERROR = s('▲', 'x'); -export const STEP_SUBMIT = s('◇', 'o'); - -export const BAR_START = s('┌', 'T'); -export const BAR = s('│', '|'); -export const BAR_END = s('└', '—'); - -export const RADIO_ACTIVE = s('●', '>'); -export const RADIO_INACTIVE = s('○', ' '); -export const CHECKBOX_ACTIVE = s('◻', '[•]'); -export const CHECKBOX_SELECTED = s('◼', '[+]'); -export const CHECKBOX_INACTIVE = s('◻', '[ ]'); -export const PASSWORD_MASK = s('▪', '•'); - -export const BAR_H = s('─', '-'); -export const CORNER_TOP_RIGHT = s('╮', '+'); -export const CONNECT_LEFT = s('├', '+'); -export const CORNER_BOTTOM_RIGHT = s('╯', '+'); - -export const INFO = s('●', '•'); -export const SUCCESS = s('◆', '*'); -export const WARN = s('▲', '!'); -export const ERROR = s('■', 'x'); diff --git a/packages/core/src/themes/types.ts b/packages/core/src/themes/types.ts deleted file mode 100644 index 14313ae5..00000000 --- a/packages/core/src/themes/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type Prompt from '../prompts/prompt'; - -export interface TemplateOptions { - ctx: Omit; - message: string; - value: string; - valueWithCursor: string | undefined; - placeholder?: string | undefined; - error?: string | undefined; -} diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index cc559bc5..2c3c81d1 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -11,7 +11,6 @@ import { block, isCancel, } from "@clack/core"; -import { defaultTheme, S } from "@clack/core/themes"; import isUnicodeSupported from "is-unicode-supported"; import color from "picocolors"; import { cursor, erase } from "sisteransi"; @@ -19,6 +18,48 @@ import { cursor, erase } from "sisteransi"; export { isCancel, setGlobalAliases } from "@clack/core"; const unicode = isUnicodeSupported(); +const s = (c: string, fallback: string) => (unicode ? c : fallback); + +const S_STEP_ACTIVE = s('◆', '*'); +const S_STEP_CANCEL = s('■', 'x'); +const S_STEP_ERROR = s('▲', 'x'); +const S_STEP_SUBMIT = s('◇', 'o'); + +const S_BAR_START = s('┌', 'T'); +const S_BAR = s('│', '|'); +const S_BAR_END = s('└', '—'); + +const S_RADIO_ACTIVE = s('●', '>'); +const S_RADIO_INACTIVE = s('○', ' '); +const S_CHECKBOX_ACTIVE = s('◻', '[•]'); +const S_CHECKBOX_SELECTED = s('◼', '[+]'); +const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); +const S_PASSWORD_MASK = s('▪', '•'); + +const S_BAR_H = s('─', '-'); +const S_CORNER_TOP_RIGHT = s('╮', '+'); +const S_CONNECT_LEFT = s('├', '+'); +const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); + +const S_INFO = s('●', '•'); +const S_SUCCESS = s('◆', '*'); +const S_WARN = s('▲', '!'); +const S_ERROR = s('■', 'x'); + +const symbol = (state: State) => { + switch (state) { + case 'initial': + case 'active': + return color.cyan(S_STEP_ACTIVE); + case 'cancel': + return color.red(S_STEP_CANCEL); + case 'error': + return color.yellow(S_STEP_ERROR); + case 'submit': + return color.green(S_STEP_SUBMIT); + } +}; + const format = Prompt.prototype.format; interface LimitOptionsParams { @@ -65,6 +106,100 @@ const limitOptions = ( }); }; +interface ThemeParams { + ctx: Omit; + message: string; + value: string; + valueWithCursor: string | undefined; + placeholder?: string | undefined; + error?: string | undefined; +} + +function applyTheme(data: ThemeParams): string { + const { ctx, message } = data; + + const title = [ + color.gray(S_BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.gray(S_BAR), + }, + }), + ].join('\n'); + + const placeholder = data.placeholder + ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) + : color.inverse(color.hidden('_')); + + const value = data.value ?? ''; + + switch (ctx.state) { + case 'cancel': + return [ + title, + format(value, { + default: { + start: color.gray(S_BAR), + style: (line) => color.strikethrough(color.dim(line)), + }, + }), + ].join('\n'); + + case 'error': + return [ + title, + format(value, { + default: { + start: color.yellow(S_BAR), + }, + }), + data.error ?? + format(ctx.error, { + default: { + start: color.yellow(S_BAR), + style: color.yellow, + }, + lastLine: { + start: color.yellow(S_BAR_END), + }, + }), + ].join('\n'); + + case 'submit': + return [ + title, + format(value, { + default: { + start: color.gray(S_BAR), + style: color.dim, + }, + }), + ].join('\n'); + + default: + return [ + color.gray(S_BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.cyan(S_BAR), + }, + }), + format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { + default: { + start: color.cyan(S_BAR), + }, + }), + color.cyan(S_BAR_END), + ].join('\n'); + } +} + export interface TextOptions { message: string; placeholder?: string; @@ -79,7 +214,7 @@ export const text = (opts: TextOptions) => { defaultValue: opts.defaultValue, initialValue: opts.initialValue, render() { - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value: this.value, @@ -98,9 +233,9 @@ export interface PasswordOptions { export const password = (opts: PasswordOptions) => { return new PasswordPrompt({ validate: opts.validate, - mask: opts.mask ?? S.PASSWORD_MASK, + mask: opts.mask ?? S_PASSWORD_MASK, render() { - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value: this.value, @@ -126,10 +261,10 @@ export const confirm = (opts: ConfirmOptions) => { render() { const opt = (state: boolean, message: string): string => { return state - ? `${color.green(S.RADIO_ACTIVE)} ${message}` - : `${color.dim(S.RADIO_INACTIVE)} ${color.dim(message)}`; + ? `${color.green(S_RADIO_ACTIVE)} ${message}` + : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(message)}`; }; - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value: @@ -170,13 +305,13 @@ export const select = (opts: SelectOptions) => { case "selected": return `${color.dim(label)}`; case "active": - return `${color.green(S.RADIO_ACTIVE)} ${label} ${ + return `${color.green(S_RADIO_ACTIVE)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : "" }`; case "cancelled": return `${color.strikethrough(color.dim(label))}`; default: - return `${color.dim(S.RADIO_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; } }; @@ -202,7 +337,7 @@ export const select = (opts: SelectOptions) => { break; } } - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value, @@ -247,7 +382,7 @@ export const selectKey = (opts: SelectOptions) => { this.options.find((opt) => opt.value === this.value)!, "selected" ); - break; + break case "cancel": value = opt(this.options[0], "cancelled"); break; @@ -260,7 +395,7 @@ export const selectKey = (opts: SelectOptions) => { break; } - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value, @@ -291,25 +426,25 @@ export const multiselect = (opts: MultiSelectOptions) => { ) => { const label = option.label ?? String(option.value); if (state === "active") { - return `${color.cyan(S.CHECKBOX_ACTIVE)} ${label} ${ + return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : "" }`; } if (state === "selected") { - return `${color.green(S.CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } if (state === "cancelled") { return `${color.strikethrough(color.dim(label))}`; } if (state === "active-selected") { - return `${color.green(S.CHECKBOX_SELECTED)} ${label} ${ + return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : "" }`; } if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; }; return new MultiSelectPrompt({ @@ -369,7 +504,7 @@ export const multiselect = (opts: MultiSelectOptions) => { .join("\n"), { firstLine: { - start: color.yellow(S.BAR_END), + start: color.yellow(S_BAR_END), }, default: { start: color.hidden("-"), @@ -394,7 +529,7 @@ export const multiselect = (opts: MultiSelectOptions) => { break; } } - return defaultTheme({ + return applyTheme({ ctx: this, message: opts.message, value, @@ -433,22 +568,22 @@ export const groupMultiselect = ( const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); const isLast = isItem && (next as any).group === true; - const prefix = isItem ? `${isLast ? S.BAR_END : S.BAR} ` : ""; + const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ""; if (state === "active") { - return `${color.dim(prefix)}${color.cyan(S.CHECKBOX_ACTIVE)} ${label} ${ + return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ option.hint ? color.dim(`(${option.hint})`) : "" }`; } if (state === "group-active") { - return `${prefix}${color.cyan(S.CHECKBOX_ACTIVE)} ${color.dim(label)}`; + return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; } if (state === "group-active-selected") { - return `${prefix}${color.green(S.CHECKBOX_SELECTED)} ${color.dim(label)}`; + return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } if (state === "selected") { return `${color.dim(prefix)}${color.green( - S.CHECKBOX_SELECTED + S_CHECKBOX_SELECTED )} ${color.dim(label)}`; } if (state === "cancelled") { @@ -456,13 +591,13 @@ export const groupMultiselect = ( } if (state === "active-selected") { return `${color.dim(prefix)}${color.green( - S.CHECKBOX_SELECTED + S_CHECKBOX_SELECTED )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ""}`; } if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(prefix)}${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim( + return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim( label )}`; }; @@ -489,23 +624,23 @@ export const groupMultiselect = ( switch (state) { case "initial": case "active": - return color.cyan(S.STEP_ACTIVE); + return color.cyan(S_STEP_ACTIVE); case "cancel": - return color.red(S.STEP_CANCEL); + return color.red(S_STEP_CANCEL); case "error": - return color.yellow(S.STEP_ERROR); + return color.yellow(S_STEP_ERROR); case "submit": - return color.green(S.STEP_SUBMIT); + return color.green(S_STEP_SUBMIT); } }; - const title = `${color.gray(S.BAR)}\n${symbol(this.state)} ${ + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ opts.message }\n`; switch (this.state) { case "submit": { - return `${title}\n${color.gray(S.BAR)} ${this.options + return `${title}\n${color.gray(S_BAR)} ${this.options .filter(({ value }) => this.value.includes(value)) .map((option) => opt(option, "submitted")) .join(color.dim(", "))}`; @@ -515,8 +650,8 @@ export const groupMultiselect = ( .filter(({ value }) => this.value.includes(value)) .map((option) => opt(option, "cancelled")) .join(color.dim(", ")); - return `${title}\n${color.gray(S.BAR)} ${ - label.trim() ? `${label}\n${color.gray(S.BAR)}` : "" + return `${title}\n${color.gray(S_BAR)} ${ + label.trim() ? `${label}\n${color.gray(S_BAR)}` : "" }`; } case "error": { @@ -524,11 +659,11 @@ export const groupMultiselect = ( .split("\n") .map((ln, i) => i === 0 - ? `${color.yellow(S.BAR_END)} ${color.yellow(ln)}` + ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` ) .join("\n"); - return `${title}\n${color.yellow(S.BAR)} ${this.options + return `${title}\n${color.yellow(S_BAR)} ${this.options .map((option, i, options) => { const selected = this.value.includes(option.value) || @@ -554,10 +689,10 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.yellow(S.BAR)} `)}\n${footer}\n`; + .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; } default: { - return `${title}\n${color.cyan(S.BAR)} ${this.options + return `${title}\n${color.cyan(S_BAR)} ${this.options .map((option, i, options) => { const selected = this.value.includes(option.value) || @@ -583,7 +718,7 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } } }, @@ -607,7 +742,7 @@ export const note = (message = "", title = "") => { const maxWidth = Math.floor((process.stdout.columns ?? 80) * 0.8); const lines = format(message, { default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), }, maxWidth: maxWidth - 2, }).split(/\n/g); @@ -618,21 +753,21 @@ export const note = (message = "", title = "") => { }, 0); const len = Math.min(Math.max(messageLen, titleLen) + 2, maxWidth); const noteBox = [ - color.gray(S.BAR), - `${color.green(S.STEP_SUBMIT)} ${color.reset(title)} ${color.gray( - S.BAR_H.repeat(Math.max(len - titleLen - 3, 0)) + S.CORNER_TOP_RIGHT + color.gray(S_BAR), + `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( + S_BAR_H.repeat(Math.max(len - titleLen - 3, 0)) + S_CORNER_TOP_RIGHT )}`, - color.gray(S.BAR + " ".repeat(len) + S.BAR), + color.gray(S_BAR + " ".repeat(len) + S_BAR), lines .map( (line) => line + " ".repeat(Math.max(len + (unicode ? 2 : 1) - strLength(line), 0)) + - color.gray(S.BAR) + color.gray(S_BAR) ) .join("\n"), - color.gray(S.BAR + " ".repeat(len) + S.BAR), - color.gray(S.CONNECT_LEFT + S.BAR_H.repeat(len) + S.CORNER_BOTTOM_RIGHT), + color.gray(S_BAR + " ".repeat(len) + S_BAR), + color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len) + S_CORNER_BOTTOM_RIGHT), "", ].join("\n"); process.stdout.write(noteBox); @@ -642,11 +777,11 @@ export const cancel = (message = "") => { process.stdout.write( `${format(message, { default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), style: color.red, }, lastLine: { - start: color.gray(S.BAR_END), + start: color.gray(S_BAR_END), }, })}\n\n` ); @@ -656,10 +791,10 @@ export const intro = (title = "") => { process.stdout.write( `${format(title, { firstLine: { - start: color.gray(S.BAR_START), + start: color.gray(S_BAR_START), }, default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), }, })}\n` ); @@ -668,13 +803,13 @@ export const intro = (title = "") => { export const outro = (message = "") => { process.stdout.write( [ - color.gray(S.BAR), + color.gray(S_BAR), format(message, { default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), }, lastLine: { - start: color.gray(S.BAR_END), + start: color.gray(S_BAR_END), }, }), "", @@ -689,7 +824,7 @@ export type LogMessageOptions = { export const log = { message: ( message = "", - { symbol = color.gray(S.BAR) }: LogMessageOptions = {} + { symbol = color.gray(S_BAR) }: LogMessageOptions = {} ) => { process.stdout.write( `${format(message, { @@ -697,29 +832,29 @@ export const log = { start: symbol, }, default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), }, })}\n` ); }, info: (message: string) => { log.message(message, { - symbol: color.blue(S.INFO), + symbol: color.blue(S_INFO), }); }, success: (message: string) => { log.message(message, { - symbol: color.green(S.SUCCESS), + symbol: color.green(S_SUCCESS), }); }, step: (message: string) => { log.message(message, { - symbol: color.green(S.STEP_SUBMIT), + symbol: color.green(S_STEP_SUBMIT), }); }, warn: (message: string) => { log.message(message, { - symbol: color.yellow(S.WARN), + symbol: color.yellow(S_WARN), }); }, /** alias for `log.warn()`. */ @@ -728,7 +863,7 @@ export const log = { }, error: (message: string) => { log.message(message, { - symbol: color.red(S.ERROR), + symbol: color.red(S_ERROR), }); }, }; @@ -750,7 +885,7 @@ export const spinner = () => { start: symbol, }, default: { - start: color.gray(S.BAR), + start: color.gray(S_BAR), }, }); }; @@ -798,7 +933,7 @@ export const spinner = () => { isSpinnerActive = true; unblock = block(); _message = parseMessage(msg); - process.stdout.write(`${color.gray(S.BAR)}\n`); + process.stdout.write(`${color.gray(S_BAR)}\n`); let frameIndex = 0; let dotsTimer = 0; registerHooks(); @@ -825,10 +960,10 @@ export const spinner = () => { clearPrevMessage(); const step = code === 0 - ? color.green(S.STEP_SUBMIT) + ? color.green(S_STEP_SUBMIT) : code === 1 - ? color.red(S.STEP_CANCEL) - : color.red(S.STEP_ERROR); + ? color.red(S_STEP_CANCEL) + : color.red(S_STEP_ERROR); _message = parseMessage(msg ?? _message); process.stdout.write(`${step} ${_message}\n`); clearHooks(); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2a6bf688..6113bdee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -58,9 +58,6 @@ importers: packages/core: dependencies: - is-unicode-supported: - specifier: ^1.3.0 - version: 1.3.0 picocolors: specifier: ^1.0.0 version: 1.0.0 @@ -3257,7 +3254,7 @@ snapshots: fastq: 1.17.1 '@rollup/plugin-alias@5.1.1(rollup@3.29.5)': - optionalDependencies: + dependencies: rollup: 3.29.5 '@rollup/plugin-commonjs@25.0.8(rollup@3.29.5)': @@ -3268,13 +3265,11 @@ snapshots: glob: 8.1.0 is-reference: 1.2.1 magic-string: 0.30.13 - optionalDependencies: rollup: 3.29.5 '@rollup/plugin-json@6.1.0(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.3(rollup@3.29.5) - optionalDependencies: rollup: 3.29.5 '@rollup/plugin-node-resolve@15.3.0(rollup@3.29.5)': @@ -3284,14 +3279,12 @@ snapshots: deepmerge: 4.3.1 is-module: 1.0.0 resolve: 1.22.8 - optionalDependencies: rollup: 3.29.5 '@rollup/plugin-replace@5.0.7(rollup@3.29.5)': dependencies: '@rollup/pluginutils': 5.1.3(rollup@3.29.5) magic-string: 0.30.13 - optionalDependencies: rollup: 3.29.5 '@rollup/pluginutils@5.1.3(rollup@3.29.5)': @@ -3299,7 +3292,6 @@ snapshots: '@types/estree': 1.0.6 estree-walker: 2.0.2 picomatch: 4.0.2 - optionalDependencies: rollup: 3.29.5 '@rollup/rollup-android-arm-eabi@4.27.3': @@ -4001,7 +3993,7 @@ snapshots: reusify: 1.0.4 fdir@6.4.2(picomatch@4.0.2): - optionalDependencies: + dependencies: picomatch: 4.0.2 fill-range@7.1.1: @@ -4434,7 +4426,6 @@ snapshots: postcss-nested: 6.2.0(postcss@8.4.49) semver: 7.6.3 tinyglobby: 0.2.10 - optionalDependencies: typescript: 5.2.2 mlly@1.7.3: @@ -5152,9 +5143,8 @@ snapshots: rollup: 3.29.5 rollup-plugin-dts: 6.1.1(rollup@3.29.5)(typescript@5.2.2) scule: 1.3.0 - untyped: 1.5.1 - optionalDependencies: typescript: 5.2.2 + untyped: 1.5.1 transitivePeerDependencies: - sass - supports-color @@ -5207,15 +5197,16 @@ snapshots: vite@5.4.11(@types/node@18.16.0): dependencies: + '@types/node': 18.16.0 esbuild: 0.21.5 postcss: 8.4.49 rollup: 4.27.3 optionalDependencies: - '@types/node': 18.16.0 fsevents: 2.3.3 vitest@1.6.0(@types/node@18.16.0): dependencies: + '@types/node': 18.16.0 '@vitest/expect': 1.6.0 '@vitest/runner': 1.6.0 '@vitest/snapshot': 1.6.0 @@ -5236,8 +5227,6 @@ snapshots: vite: 5.4.11(@types/node@18.16.0) vite-node: 1.6.0(@types/node@18.16.0) why-is-node-running: 2.3.0 - optionalDependencies: - '@types/node': 18.16.0 transitivePeerDependencies: - less - lightningcss @@ -5372,4 +5361,4 @@ snapshots: yocto-queue@0.1.0: {} yocto-queue@1.1.1: {} - \ No newline at end of file + From 01e4ad6b0f52c9f7aca11778e463c9f0eb16a1f9 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Sun, 20 Aug 2023 21:33:30 -0300 Subject: [PATCH 11/20] refactor: restore default select key --- packages/prompts/src/index.ts | 124 +++++++++++++++++----------------- 1 file changed, 62 insertions(+), 62 deletions(-) diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2c3c81d1..091fd381 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -20,42 +20,42 @@ export { isCancel, setGlobalAliases } from "@clack/core"; const unicode = isUnicodeSupported(); const s = (c: string, fallback: string) => (unicode ? c : fallback); -const S_STEP_ACTIVE = s('◆', '*'); -const S_STEP_CANCEL = s('■', 'x'); -const S_STEP_ERROR = s('▲', 'x'); -const S_STEP_SUBMIT = s('◇', 'o'); - -const S_BAR_START = s('┌', 'T'); -const S_BAR = s('│', '|'); -const S_BAR_END = s('└', '—'); - -const S_RADIO_ACTIVE = s('●', '>'); -const S_RADIO_INACTIVE = s('○', ' '); -const S_CHECKBOX_ACTIVE = s('◻', '[•]'); -const S_CHECKBOX_SELECTED = s('◼', '[+]'); -const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); -const S_PASSWORD_MASK = s('▪', '•'); - -const S_BAR_H = s('─', '-'); -const S_CORNER_TOP_RIGHT = s('╮', '+'); -const S_CONNECT_LEFT = s('├', '+'); -const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); - -const S_INFO = s('●', '•'); -const S_SUCCESS = s('◆', '*'); -const S_WARN = s('▲', '!'); -const S_ERROR = s('■', 'x'); +const S_STEP_ACTIVE = s("◆", "*"); +const S_STEP_CANCEL = s("■", "x"); +const S_STEP_ERROR = s("▲", "x"); +const S_STEP_SUBMIT = s("◇", "o"); + +const S_BAR_START = s("┌", "T"); +const S_BAR = s("│", "|"); +const S_BAR_END = s("└", "—"); + +const S_RADIO_ACTIVE = s("●", ">"); +const S_RADIO_INACTIVE = s("○", " "); +const S_CHECKBOX_ACTIVE = s("◻", "[•]"); +const S_CHECKBOX_SELECTED = s("◼", "[+]"); +const S_CHECKBOX_INACTIVE = s("◻", "[ ]"); +const S_PASSWORD_MASK = s("▪", "•"); + +const S_BAR_H = s("─", "-"); +const S_CORNER_TOP_RIGHT = s("╮", "+"); +const S_CONNECT_LEFT = s("├", "+"); +const S_CORNER_BOTTOM_RIGHT = s("╯", "+"); + +const S_INFO = s("●", "•"); +const S_SUCCESS = s("◆", "*"); +const S_WARN = s("▲", "!"); +const S_ERROR = s("■", "x"); const symbol = (state: State) => { switch (state) { - case 'initial': - case 'active': + case "initial": + case "active": return color.cyan(S_STEP_ACTIVE); - case 'cancel': + case "cancel": return color.red(S_STEP_CANCEL); - case 'error': + case "error": return color.yellow(S_STEP_ERROR); - case 'submit': + case "submit": return color.green(S_STEP_SUBMIT); } }; @@ -107,7 +107,7 @@ const limitOptions = ( }; interface ThemeParams { - ctx: Omit; + ctx: Omit; message: string; value: string; valueWithCursor: string | undefined; @@ -128,16 +128,16 @@ function applyTheme(data: ThemeParams): string { start: color.gray(S_BAR), }, }), - ].join('\n'); + ].join("\n"); const placeholder = data.placeholder ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) - : color.inverse(color.hidden('_')); + : color.inverse(color.hidden("_")); - const value = data.value ?? ''; + const value = data.value ?? ""; switch (ctx.state) { - case 'cancel': + case "cancel": return [ title, format(value, { @@ -146,9 +146,9 @@ function applyTheme(data: ThemeParams): string { style: (line) => color.strikethrough(color.dim(line)), }, }), - ].join('\n'); + ].join("\n"); - case 'error': + case "error": return [ title, format(value, { @@ -166,9 +166,9 @@ function applyTheme(data: ThemeParams): string { start: color.yellow(S_BAR_END), }, }), - ].join('\n'); + ].join("\n"); - case 'submit': + case "submit": return [ title, format(value, { @@ -177,7 +177,7 @@ function applyTheme(data: ThemeParams): string { style: color.dim, }, }), - ].join('\n'); + ].join("\n"); default: return [ @@ -190,13 +190,18 @@ function applyTheme(data: ThemeParams): string { start: color.cyan(S_BAR), }, }), - format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { - default: { - start: color.cyan(S_BAR), - }, - }), + format( + data.placeholder && !data.value + ? placeholder + : data.valueWithCursor ?? value, + { + default: { + start: color.cyan(S_BAR), + }, + } + ), color.cyan(S_BAR_END), - ].join('\n'); + ].join("\n"); } } @@ -373,34 +378,29 @@ export const selectKey = (opts: SelectOptions) => { options: opts.options, initialValue: opts.initialValue, render() { - let value: string; + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ + opts.message + }\n`; switch (this.state) { case "submit": - value = opt( + return `${title}${color.gray(S_BAR)} ${opt( // biome-ignore lint/style/noNonNullAssertion: this.options.find((opt) => opt.value === this.value)!, "selected" - ); - break + )}`; case "cancel": - value = opt(this.options[0], "cancelled"); - break; + return `${title}${color.gray(S_BAR)} ${opt( + this.options[0], + "cancelled" + )}\n${color.gray(S_BAR)}`; default: - value = this.options + return `${title}${color.cyan(S_BAR)} ${this.options .map((option, i) => opt(option, i === this.cursor ? "active" : "inactive") ) - .join("\n"); - break; + .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; } - - return applyTheme({ - ctx: this, - message: opts.message, - value, - valueWithCursor: undefined, - }); }, }).prompt() as Promise; }; From 5240c6ce995ffd82427a54d4229e49865d33520a Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 21 Aug 2023 12:25:13 -0300 Subject: [PATCH 12/20] chore: add changeset --- .changeset/neat-birds-collect.md | 6 ++++++ packages/prompts/src/index.ts | 6 ++---- 2 files changed, 8 insertions(+), 4 deletions(-) create mode 100644 .changeset/neat-birds-collect.md diff --git a/.changeset/neat-birds-collect.md b/.changeset/neat-birds-collect.md new file mode 100644 index 00000000..4efe851c --- /dev/null +++ b/.changeset/neat-birds-collect.md @@ -0,0 +1,6 @@ +--- +'@clack/prompts': minor +'@clack/core': minor +--- + +Feat multiline support diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 091fd381..a218dbc5 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -396,10 +396,8 @@ export const selectKey = (opts: SelectOptions) => { )}\n${color.gray(S_BAR)}`; default: return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => - opt(option, i === this.cursor ? "active" : "inactive") - ) - .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; + .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } }, }).prompt() as Promise; From eb7ad9945c18b9ad1ff2a6e48961b2e27861cd69 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Thu, 24 Aug 2023 20:18:19 -0300 Subject: [PATCH 13/20] fix: password shift --- packages/core/src/index.ts | 2 +- packages/core/src/utils/index.ts | 44 ++++++++++++++++++++------------ packages/prompts/src/index.ts | 31 ++++------------------ 3 files changed, 33 insertions(+), 44 deletions(-) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 3084aa3b..8c830706 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -7,4 +7,4 @@ export type { State } from "./prompts/prompt"; export { default as SelectPrompt } from "./prompts/select"; export { default as SelectKeyPrompt } from "./prompts/select-key"; export { default as TextPrompt } from "./prompts/text"; -export { block, isCancel, setGlobalAliases } from "./utils"; +export { block, isCancel, setGlobalAliases, strLength } from "./utils"; diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 239ee815..3e2ccee7 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -70,22 +70,32 @@ export function block({ }; } -export function strLength(str: string) { - if (!str) return 0; - - const colorCodeRegex = /\x1B\[[0-9;]*[mG]/g; - const ansiRegex = new RegExp( - [ - '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', - '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))', - ].join('|'), - 'g' - ); - const arr = [...str.replace(colorCodeRegex, '').replace(ansiRegex, '')]; - let len = 0; - - for (const char of arr) { - len += char.charCodeAt(0) > 127 || char.charCodeAt(0) === 94 ? 2 : 1; +export function strLength(input: string) { + let length = 0; + let i = 0; + + while (i < input.length) { + if (input[i] === '\u001b') { + // Check for escape character (ANSI escape code) + const endIndex = input.indexOf('m', i + 1); // Find the end of ANSI code + if (endIndex === -1) { + i++; // Skip the escape character and continue + continue; + } else { + i = endIndex + 1; + continue; + } + } + // Handle other control codes or regular characters + const code = input.charCodeAt(i); + + if (code >= 0xd800 && code <= 0xdbff) { + i += 2; + } else { + length++; + i++; + } } - return len; + + return length; } diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index a218dbc5..790e663d 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -10,6 +10,7 @@ import { TextPrompt, block, isCancel, + strLength, } from "@clack/core"; import isUnicodeSupported from "is-unicode-supported"; import color from "picocolors"; @@ -243,7 +244,7 @@ export const password = (opts: PasswordOptions) => { return applyTheme({ ctx: this, message: opts.message, - value: this.value, + value: this.valueWithCursor, valueWithCursor: this.valueWithCursor, }); }, @@ -396,7 +397,9 @@ export const selectKey = (opts: SelectOptions) => { )}\n${color.gray(S_BAR)}`; default: return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) + .map((option, i) => + opt(option, i === this.cursor ? "active" : "inactive") + ) .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } }, @@ -723,19 +726,6 @@ export const groupMultiselect = ( }).prompt() as Promise; }; -const strip = (str: string) => str.replace(ansiRegex(), ""); -const strLength = (str: string) => { - if (!str) return 0; - - const colorCodeRegex = /\x1B\[[0-9;]*[mG]/g; - const arr = [...strip(str.replace(colorCodeRegex, ""))]; - let len = 0; - - for (const char of arr) { - len += char.charCodeAt(0) > 127 || char.charCodeAt(0) === 94 ? 2 : 1; - } - return len; -}; export const note = (message = "", title = "") => { const maxWidth = Math.floor((process.stdout.columns ?? 80) * 0.8); const lines = format(message, { @@ -979,17 +969,6 @@ export const spinner = () => { }; }; -// Adapted from https://github.com/chalk/ansi-regex -// @see LICENSE -function ansiRegex() { - const pattern = [ - "[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]+)*|[a-zA-Z\\d]+(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)", - "(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-nq-uy=><~]))", - ].join("|"); - - return new RegExp(pattern, "g"); -} - export type PromptGroupAwaitedReturn = { [P in keyof T]: Exclude, symbol>; }; From 25657b81a7d98c895138f5a596827e84c7fb702c Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 13 Oct 2023 15:02:05 -0300 Subject: [PATCH 14/20] refactor: strLength support more edge cases --- packages/core/src/utils/index.ts | 66 +++++++++++++++++++++----------- 1 file changed, 44 insertions(+), 22 deletions(-) diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index 3e2ccee7..d804b3aa 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -70,31 +70,53 @@ export function block({ }; } -export function strLength(input: string) { +function ansiRegex(): RegExp { + const pattern = [ + '[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)', + '(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))', + ].join('|'); + + return new RegExp(pattern, 'g'); +} + +function stripAnsi(str: string): string { + return str.replace(ansiRegex(), ''); +} + +function isControlCharacter(code: number): boolean { + return code <= 0x1f || (code >= 0x7f && code <= 0x9f); +} + +function isCombiningCharacter(code: number): boolean { + return code >= 0x300 && code <= 0x36f; +} + +function isSurrogatePair(code: number): boolean { + return code >= 0xd800 && code <= 0xdbff; +} + +export function strLength(str: string): number { + if (str === '') { + return 0; + } + + // Remove ANSI escape codes from the input string. + str = stripAnsi(str); + let length = 0; - let i = 0; - - while (i < input.length) { - if (input[i] === '\u001b') { - // Check for escape character (ANSI escape code) - const endIndex = input.indexOf('m', i + 1); // Find the end of ANSI code - if (endIndex === -1) { - i++; // Skip the escape character and continue - continue; - } else { - i = endIndex + 1; - continue; - } + + for (let i = 0; i < str.length; i++) { + const code = str.codePointAt(i); + + if (!code || isControlCharacter(code) || isCombiningCharacter(code)) { + continue; } - // Handle other control codes or regular characters - const code = input.charCodeAt(i); - - if (code >= 0xd800 && code <= 0xdbff) { - i += 2; - } else { - length++; - i++; + + if (isSurrogatePair(code)) { + i++; // Skip the next code unit. } + + length++; } return length; From f1e55ebae4168cfa470dc130028395561b658509 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:30:07 -0300 Subject: [PATCH 15/20] feat: default theme --- packages/core/src/themes/default.ts | 105 ++++++++++++++++++++++++++++ packages/core/src/themes/index.ts | 3 + packages/core/src/themes/symbols.ts | 30 ++++++++ packages/core/src/themes/types.ts | 10 +++ 4 files changed, 148 insertions(+) create mode 100644 packages/core/src/themes/default.ts create mode 100644 packages/core/src/themes/index.ts create mode 100644 packages/core/src/themes/symbols.ts create mode 100644 packages/core/src/themes/types.ts diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts new file mode 100644 index 00000000..f8529218 --- /dev/null +++ b/packages/core/src/themes/default.ts @@ -0,0 +1,105 @@ +import Prompt, { State } from '../prompts/prompt'; +import { TemplateOptions } from './types'; +import * as S from './symbols'; +import color from 'picocolors'; + +const symbol = (state: State) => { + switch (state) { + case 'initial': + case 'active': + return color.cyan(S.STEP_ACTIVE); + case 'cancel': + return color.red(S.STEP_CANCEL); + case 'error': + return color.yellow(S.STEP_ERROR); + case 'submit': + return color.green(S.STEP_SUBMIT); + } +}; + +const format = Prompt.prototype.format; + +export function template(data: TemplateOptions): string { + const { ctx, message } = data; + + const title = [ + color.gray(S.BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.gray(S.BAR), + }, + }), + ].join('\n'); + + const placeholder = data.placeholder + ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) + : color.inverse(color.hidden('_')); + + const value = data.value ?? ''; + + switch (ctx.state) { + case 'cancel': + return [ + title, + format(value, { + default: { + start: color.gray(S.BAR), + style: (line) => color.strikethrough(color.dim(line)), + }, + }), + ].join('\n'); + + case 'error': + return [ + title, + format(value, { + default: { + start: color.yellow(S.BAR), + }, + }), + data.error ?? + format(ctx.error, { + default: { + start: color.yellow(S.BAR), + style: color.yellow, + }, + lastLine: { + start: color.yellow(S.BAR_END), + }, + }), + ].join('\n'); + + case 'submit': + return [ + title, + format(value, { + default: { + start: color.gray(S.BAR), + style: color.dim, + }, + }), + ].join('\n'); + + default: + return [ + color.gray(S.BAR), + format(message, { + firstLine: { + start: symbol(ctx.state), + }, + default: { + start: color.cyan(S.BAR), + }, + }), + format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { + default: { + start: color.cyan(S.BAR), + }, + }), + color.cyan(S.BAR_END), + ].join('\n'); + } +} diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts new file mode 100644 index 00000000..cce227a3 --- /dev/null +++ b/packages/core/src/themes/index.ts @@ -0,0 +1,3 @@ +export * from './types'; +export * as S from './symbols'; +export { template as defaultTheme } from './default'; diff --git a/packages/core/src/themes/symbols.ts b/packages/core/src/themes/symbols.ts new file mode 100644 index 00000000..599748ab --- /dev/null +++ b/packages/core/src/themes/symbols.ts @@ -0,0 +1,30 @@ +import isUnicodeSupported from 'is-unicode-supported'; + +const unicode = isUnicodeSupported(); +const s = (c: string, fallback: string) => (unicode ? c : fallback); + +export const STEP_ACTIVE = s('◆', '*'); +export const STEP_CANCEL = s('■', 'x'); +export const STEP_ERROR = s('▲', 'x'); +export const STEP_SUBMIT = s('◇', 'o'); + +export const BAR_START = s('┌', 'T'); +export const BAR = s('│', '|'); +export const BAR_END = s('└', '—'); + +export const RADIO_ACTIVE = s('●', '>'); +export const RADIO_INACTIVE = s('○', ' '); +export const CHECKBOX_ACTIVE = s('◻', '[•]'); +export const CHECKBOX_SELECTED = s('◼', '[+]'); +export const CHECKBOX_INACTIVE = s('◻', '[ ]'); +export const PASSWORD_MASK = s('▪', '•'); + +export const BAR_H = s('─', '-'); +export const CORNER_TOP_RIGHT = s('╮', '+'); +export const CONNECT_LEFT = s('├', '+'); +export const CORNER_BOTTOM_RIGHT = s('╯', '+'); + +export const INFO = s('●', '•'); +export const SUCCESS = s('◆', '*'); +export const WARN = s('▲', '!'); +export const ERROR = s('■', 'x'); diff --git a/packages/core/src/themes/types.ts b/packages/core/src/themes/types.ts new file mode 100644 index 00000000..a3ee8653 --- /dev/null +++ b/packages/core/src/themes/types.ts @@ -0,0 +1,10 @@ +import Prompt from '../prompts/prompt'; + +export interface TemplateOptions { + ctx: Omit; + message: string; + value: string; + valueWithCursor: string | undefined; + placeholder?: string | undefined; + error?: string | undefined; +} From 19d74c7e700745634fa416bcd259b61fd4b87793 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:30:57 -0300 Subject: [PATCH 16/20] feat: theme package --- packages/core/build.config.ts | 11 ++++++++++- packages/core/package.json | 13 +++++++++++++ packages/core/src/index.ts | 10 ---------- packages/core/src/prompts/index.ts | 10 ++++++++++ 4 files changed, 33 insertions(+), 11 deletions(-) delete mode 100644 packages/core/src/index.ts create mode 100644 packages/core/src/prompts/index.ts diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index 8bd1ad27..a237314e 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -3,5 +3,14 @@ import { defineBuildConfig } from 'unbuild'; // @see https://github.com/unjs/unbuild export default defineBuildConfig({ preset: '../../build.preset', - entries: ['src/index'], + entries: [ + { + name: 'index', + input: 'src/prompts/index', + }, + { + name: 'themes', + input: 'src/themes/index', + }, + ], }); diff --git a/packages/core/package.json b/packages/core/package.json index 753e30ba..29f36bb8 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,9 +10,21 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, + "./themes": { + "types": "./dist/themes.d.ts", + "import": "./dist/themes.mjs", + "require": "./dist/themes.cjs" + }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts", + "typesVersions": { + "*": { + "themes": [ + "./dist/themes.d.ts" + ] + } + }, "repository": { "type": "git", "url": "https://github.com/natemoo-re/clack", @@ -51,6 +63,7 @@ "test": "vitest run" }, "dependencies": { + "is-unicode-supported": "^1.3.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts deleted file mode 100644 index 8c830706..00000000 --- a/packages/core/src/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { default as ConfirmPrompt } from "./prompts/confirm"; -export { default as GroupMultiSelectPrompt } from "./prompts/group-multiselect"; -export { default as MultiSelectPrompt } from "./prompts/multi-select"; -export { default as PasswordPrompt } from "./prompts/password"; -export { default as Prompt } from "./prompts/prompt"; -export type { State } from "./prompts/prompt"; -export { default as SelectPrompt } from "./prompts/select"; -export { default as SelectKeyPrompt } from "./prompts/select-key"; -export { default as TextPrompt } from "./prompts/text"; -export { block, isCancel, setGlobalAliases, strLength } from "./utils"; diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts new file mode 100644 index 00000000..86c63441 --- /dev/null +++ b/packages/core/src/prompts/index.ts @@ -0,0 +1,10 @@ +export { default as ConfirmPrompt } from "./confirm"; +export { default as GroupMultiSelectPrompt } from "./group-multiselect"; +export { default as MultiSelectPrompt } from "./multi-select"; +export { default as PasswordPrompt } from "./password"; +export { default as Prompt } from "./prompt"; +export type { State } from "./prompt"; +export { default as SelectPrompt } from "./select"; +export { default as SelectKeyPrompt } from "./select-key"; +export { default as TextPrompt } from "./text"; +export { block, isCancel, strLength, setGlobalAliases } from "../utils"; From 38231c07a29546e1b698499ff504255aaaeef231 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:34:08 -0300 Subject: [PATCH 17/20] refactor: prompts with default theme --- packages/prompts/src/index.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 790e663d..6c36c4c3 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -17,6 +17,7 @@ import color from "picocolors"; import { cursor, erase } from "sisteransi"; export { isCancel, setGlobalAliases } from "@clack/core"; +import { defaultTheme, S } from '@clack/core/themes'; const unicode = isUnicodeSupported(); const s = (c: string, fallback: string) => (unicode ? c : fallback); @@ -239,7 +240,7 @@ export interface PasswordOptions { export const password = (opts: PasswordOptions) => { return new PasswordPrompt({ validate: opts.validate, - mask: opts.mask ?? S_PASSWORD_MASK, + mask: opts.mask ?? S.PASSWORD_MASK, render() { return applyTheme({ ctx: this, @@ -445,7 +446,7 @@ export const multiselect = (opts: MultiSelectOptions) => { if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim(label)}`; }; return new MultiSelectPrompt({ @@ -690,7 +691,7 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; + .join(`\n${color.yellow(S.BAR)} `)}\n${footer}\n`; } default: { return `${title}\n${color.cyan(S_BAR)} ${this.options @@ -719,7 +720,7 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; + .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; } } }, @@ -948,7 +949,7 @@ export const spinner = () => { clearPrevMessage(); const step = code === 0 - ? color.green(S_STEP_SUBMIT) + ? color.green(S.STEP_SUBMIT) : code === 1 ? color.red(S_STEP_CANCEL) : color.red(S_STEP_ERROR); From c8113742142f446040f10e53e079281b35ee68e6 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Fri, 18 Aug 2023 14:41:33 -0300 Subject: [PATCH 18/20] fix: ci --- packages/core/src/themes/default.ts | 4 +- packages/core/src/themes/index.ts | 4 +- pnpm-lock.yaml | 68 +++++++++++++++++------------ 3 files changed, 43 insertions(+), 33 deletions(-) diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts index f8529218..a092317a 100644 --- a/packages/core/src/themes/default.ts +++ b/packages/core/src/themes/default.ts @@ -1,7 +1,7 @@ +import color from 'picocolors'; import Prompt, { State } from '../prompts/prompt'; -import { TemplateOptions } from './types'; import * as S from './symbols'; -import color from 'picocolors'; +import { TemplateOptions } from './types'; const symbol = (state: State) => { switch (state) { diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts index cce227a3..d26b9cb9 100644 --- a/packages/core/src/themes/index.ts +++ b/packages/core/src/themes/index.ts @@ -1,3 +1,3 @@ -export * from './types'; -export * as S from './symbols'; export { template as defaultTheme } from './default'; +export * as S from './symbols'; +export * from './types'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6113bdee..b461cbc2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,14 @@ settings: importers: .: + specifiers: + '@changesets/cli': ^2.26.0 + '@types/node': '18' + merge2: ^1.4.1 + organize-imports-cli: ^0.10.0 + prettier: ^2.8.4 + typescript: ^4.9.5 + unbuild: 1.1.2 devDependencies: '@biomejs/biome': specifier: 1.9.4 @@ -25,38 +33,37 @@ importers: version: 2.0.0(typescript@5.2.2) examples/basic: - dependencies: - '@clack/core': - specifier: workspace:* - version: link:../../packages/core - '@clack/prompts': - specifier: workspace:* - version: link:../../packages/prompts - picocolors: - specifier: ^1.0.0 - version: 1.0.0 + specifiers: + '@clack/core': workspace:* + '@clack/prompts': workspace:* + jiti: ^1.17.0 + picocolors: ^1.0.0 + dependencies: + '@clack/core': link:../../packages/core + '@clack/prompts': link:../../packages/prompts + picocolors: 1.0.0 devDependencies: - jiti: - specifier: ^1.17.0 - version: 1.17.0 + jiti: 1.19.3 examples/changesets: - dependencies: - '@clack/core': - specifier: workspace:* - version: link:../../packages/core - '@clack/prompts': - specifier: workspace:* - version: link:../../packages/prompts - picocolors: - specifier: ^1.0.0 - version: 1.0.0 + specifiers: + '@clack/core': workspace:* + '@clack/prompts': workspace:* + jiti: ^1.17.0 + picocolors: ^1.0.0 + dependencies: + '@clack/core': link:../../packages/core + '@clack/prompts': link:../../packages/prompts + picocolors: 1.0.0 devDependencies: - jiti: - specifier: ^1.17.0 - version: 1.17.0 + jiti: 1.19.3 packages/core: + specifiers: + is-unicode-supported: ^1.3.0 + picocolors: ^1.0.0 + sisteransi: ^1.0.5 + wrap-ansi: ^8.1.0 dependencies: picocolors: specifier: ^1.0.0 @@ -73,6 +80,11 @@ importers: version: 8.1.0 packages/prompts: + specifiers: + '@clack/core': ^0.3.3 + is-unicode-supported: ^1.3.0 + picocolors: ^1.0.0 + sisteransi: ^1.0.5 dependencies: '@clack/core': specifier: workspace:* @@ -84,9 +96,7 @@ importers: specifier: ^1.0.5 version: 1.0.5 devDependencies: - is-unicode-supported: - specifier: ^1.3.0 - version: 1.3.0 + is-unicode-supported: 1.3.0 packages: From 69b703006321554df01bdb0d700ae25257543731 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Mon, 21 Aug 2023 12:07:15 -0300 Subject: [PATCH 19/20] refactor: revert themes subpath --- packages/core/build.config.ts | 11 +-- packages/core/package.json | 13 ---- packages/core/src/index.ts | 9 +++ packages/core/src/prompts/index.ts | 10 --- packages/core/src/themes/default.ts | 105 ---------------------------- packages/core/src/themes/index.ts | 3 - packages/core/src/themes/symbols.ts | 30 -------- packages/core/src/themes/types.ts | 10 --- packages/prompts/src/index.ts | 11 ++- pnpm-lock.yaml | 1 - 10 files changed, 15 insertions(+), 188 deletions(-) create mode 100644 packages/core/src/index.ts delete mode 100644 packages/core/src/prompts/index.ts delete mode 100644 packages/core/src/themes/default.ts delete mode 100644 packages/core/src/themes/index.ts delete mode 100644 packages/core/src/themes/symbols.ts delete mode 100644 packages/core/src/themes/types.ts diff --git a/packages/core/build.config.ts b/packages/core/build.config.ts index a237314e..8bd1ad27 100644 --- a/packages/core/build.config.ts +++ b/packages/core/build.config.ts @@ -3,14 +3,5 @@ import { defineBuildConfig } from 'unbuild'; // @see https://github.com/unjs/unbuild export default defineBuildConfig({ preset: '../../build.preset', - entries: [ - { - name: 'index', - input: 'src/prompts/index', - }, - { - name: 'themes', - input: 'src/themes/index', - }, - ], + entries: ['src/index'], }); diff --git a/packages/core/package.json b/packages/core/package.json index 29f36bb8..753e30ba 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,21 +10,9 @@ "import": "./dist/index.mjs", "require": "./dist/index.cjs" }, - "./themes": { - "types": "./dist/themes.d.ts", - "import": "./dist/themes.mjs", - "require": "./dist/themes.cjs" - }, "./package.json": "./package.json" }, "types": "./dist/index.d.ts", - "typesVersions": { - "*": { - "themes": [ - "./dist/themes.d.ts" - ] - } - }, "repository": { "type": "git", "url": "https://github.com/natemoo-re/clack", @@ -63,7 +51,6 @@ "test": "vitest run" }, "dependencies": { - "is-unicode-supported": "^1.3.0", "picocolors": "^1.0.0", "sisteransi": "^1.0.5" }, diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts new file mode 100644 index 00000000..9a322586 --- /dev/null +++ b/packages/core/src/index.ts @@ -0,0 +1,9 @@ +export { default as ConfirmPrompt } from "./prompts/confirm"; +export { default as GroupMultiSelectPrompt } from "./prompts/group-multiselect"; +export { default as MultiSelectPrompt } from "./prompts/multi-select"; +export { default as PasswordPrompt } from "./prompts/password"; +export { default as Prompt, State } from "./prompts/prompt"; +export { default as SelectPrompt } from "./prompts/select"; +export { default as SelectKeyPrompt } from "./prompts/select-key"; +export { default as TextPrompt } from "./prompts/text"; +export { block, isCancel, strLength, setGlobalAliases } from "./utils"; diff --git a/packages/core/src/prompts/index.ts b/packages/core/src/prompts/index.ts deleted file mode 100644 index 86c63441..00000000 --- a/packages/core/src/prompts/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -export { default as ConfirmPrompt } from "./confirm"; -export { default as GroupMultiSelectPrompt } from "./group-multiselect"; -export { default as MultiSelectPrompt } from "./multi-select"; -export { default as PasswordPrompt } from "./password"; -export { default as Prompt } from "./prompt"; -export type { State } from "./prompt"; -export { default as SelectPrompt } from "./select"; -export { default as SelectKeyPrompt } from "./select-key"; -export { default as TextPrompt } from "./text"; -export { block, isCancel, strLength, setGlobalAliases } from "../utils"; diff --git a/packages/core/src/themes/default.ts b/packages/core/src/themes/default.ts deleted file mode 100644 index a092317a..00000000 --- a/packages/core/src/themes/default.ts +++ /dev/null @@ -1,105 +0,0 @@ -import color from 'picocolors'; -import Prompt, { State } from '../prompts/prompt'; -import * as S from './symbols'; -import { TemplateOptions } from './types'; - -const symbol = (state: State) => { - switch (state) { - case 'initial': - case 'active': - return color.cyan(S.STEP_ACTIVE); - case 'cancel': - return color.red(S.STEP_CANCEL); - case 'error': - return color.yellow(S.STEP_ERROR); - case 'submit': - return color.green(S.STEP_SUBMIT); - } -}; - -const format = Prompt.prototype.format; - -export function template(data: TemplateOptions): string { - const { ctx, message } = data; - - const title = [ - color.gray(S.BAR), - format(message, { - firstLine: { - start: symbol(ctx.state), - }, - default: { - start: color.gray(S.BAR), - }, - }), - ].join('\n'); - - const placeholder = data.placeholder - ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) - : color.inverse(color.hidden('_')); - - const value = data.value ?? ''; - - switch (ctx.state) { - case 'cancel': - return [ - title, - format(value, { - default: { - start: color.gray(S.BAR), - style: (line) => color.strikethrough(color.dim(line)), - }, - }), - ].join('\n'); - - case 'error': - return [ - title, - format(value, { - default: { - start: color.yellow(S.BAR), - }, - }), - data.error ?? - format(ctx.error, { - default: { - start: color.yellow(S.BAR), - style: color.yellow, - }, - lastLine: { - start: color.yellow(S.BAR_END), - }, - }), - ].join('\n'); - - case 'submit': - return [ - title, - format(value, { - default: { - start: color.gray(S.BAR), - style: color.dim, - }, - }), - ].join('\n'); - - default: - return [ - color.gray(S.BAR), - format(message, { - firstLine: { - start: symbol(ctx.state), - }, - default: { - start: color.cyan(S.BAR), - }, - }), - format(data.placeholder && !data.value ? placeholder : data.valueWithCursor ?? value, { - default: { - start: color.cyan(S.BAR), - }, - }), - color.cyan(S.BAR_END), - ].join('\n'); - } -} diff --git a/packages/core/src/themes/index.ts b/packages/core/src/themes/index.ts deleted file mode 100644 index d26b9cb9..00000000 --- a/packages/core/src/themes/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { template as defaultTheme } from './default'; -export * as S from './symbols'; -export * from './types'; diff --git a/packages/core/src/themes/symbols.ts b/packages/core/src/themes/symbols.ts deleted file mode 100644 index 599748ab..00000000 --- a/packages/core/src/themes/symbols.ts +++ /dev/null @@ -1,30 +0,0 @@ -import isUnicodeSupported from 'is-unicode-supported'; - -const unicode = isUnicodeSupported(); -const s = (c: string, fallback: string) => (unicode ? c : fallback); - -export const STEP_ACTIVE = s('◆', '*'); -export const STEP_CANCEL = s('■', 'x'); -export const STEP_ERROR = s('▲', 'x'); -export const STEP_SUBMIT = s('◇', 'o'); - -export const BAR_START = s('┌', 'T'); -export const BAR = s('│', '|'); -export const BAR_END = s('└', '—'); - -export const RADIO_ACTIVE = s('●', '>'); -export const RADIO_INACTIVE = s('○', ' '); -export const CHECKBOX_ACTIVE = s('◻', '[•]'); -export const CHECKBOX_SELECTED = s('◼', '[+]'); -export const CHECKBOX_INACTIVE = s('◻', '[ ]'); -export const PASSWORD_MASK = s('▪', '•'); - -export const BAR_H = s('─', '-'); -export const CORNER_TOP_RIGHT = s('╮', '+'); -export const CONNECT_LEFT = s('├', '+'); -export const CORNER_BOTTOM_RIGHT = s('╯', '+'); - -export const INFO = s('●', '•'); -export const SUCCESS = s('◆', '*'); -export const WARN = s('▲', '!'); -export const ERROR = s('■', 'x'); diff --git a/packages/core/src/themes/types.ts b/packages/core/src/themes/types.ts deleted file mode 100644 index a3ee8653..00000000 --- a/packages/core/src/themes/types.ts +++ /dev/null @@ -1,10 +0,0 @@ -import Prompt from '../prompts/prompt'; - -export interface TemplateOptions { - ctx: Omit; - message: string; - value: string; - valueWithCursor: string | undefined; - placeholder?: string | undefined; - error?: string | undefined; -} diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 6c36c4c3..790e663d 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -17,7 +17,6 @@ import color from "picocolors"; import { cursor, erase } from "sisteransi"; export { isCancel, setGlobalAliases } from "@clack/core"; -import { defaultTheme, S } from '@clack/core/themes'; const unicode = isUnicodeSupported(); const s = (c: string, fallback: string) => (unicode ? c : fallback); @@ -240,7 +239,7 @@ export interface PasswordOptions { export const password = (opts: PasswordOptions) => { return new PasswordPrompt({ validate: opts.validate, - mask: opts.mask ?? S.PASSWORD_MASK, + mask: opts.mask ?? S_PASSWORD_MASK, render() { return applyTheme({ ctx: this, @@ -446,7 +445,7 @@ export const multiselect = (opts: MultiSelectOptions) => { if (state === "submitted") { return `${color.dim(label)}`; } - return `${color.dim(S.CHECKBOX_INACTIVE)} ${color.dim(label)}`; + return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; }; return new MultiSelectPrompt({ @@ -691,7 +690,7 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.yellow(S.BAR)} `)}\n${footer}\n`; + .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; } default: { return `${title}\n${color.cyan(S_BAR)} ${this.options @@ -720,7 +719,7 @@ export const groupMultiselect = ( } return opt(option, active ? "active" : "inactive", options); }) - .join(`\n${color.cyan(S.BAR)} `)}\n${color.cyan(S.BAR_END)}\n`; + .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } } }, @@ -949,7 +948,7 @@ export const spinner = () => { clearPrevMessage(); const step = code === 0 - ? color.green(S.STEP_SUBMIT) + ? color.green(S_STEP_SUBMIT) : code === 1 ? color.red(S_STEP_CANCEL) : color.red(S_STEP_ERROR); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b461cbc2..571757f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -60,7 +60,6 @@ importers: packages/core: specifiers: - is-unicode-supported: ^1.3.0 picocolors: ^1.0.0 sisteransi: ^1.0.5 wrap-ansi: ^8.1.0 From b780203b608e69b26ef8394d272dd36326fda4c8 Mon Sep 17 00:00:00 2001 From: Bruno Rocha Date: Tue, 26 Nov 2024 15:36:03 -0300 Subject: [PATCH 20/20] chore: rebase branch --- package.json | 2 +- packages/core/src/index.ts | 19 +- packages/core/src/prompts/prompt.ts | 25 +- packages/core/src/utils/index.ts | 6 +- packages/prompts/src/index.ts | 462 ++++++++++++---------------- pnpm-lock.yaml | 67 ++-- 6 files changed, 257 insertions(+), 324 deletions(-) diff --git a/package.json b/package.json index 4bd441a2..1666382f 100644 --- a/package.json +++ b/package.json @@ -25,4 +25,4 @@ "volta": { "node": "20.18.1" } -} \ No newline at end of file +} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 9a322586..a1a1dafe 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,9 +1,10 @@ -export { default as ConfirmPrompt } from "./prompts/confirm"; -export { default as GroupMultiSelectPrompt } from "./prompts/group-multiselect"; -export { default as MultiSelectPrompt } from "./prompts/multi-select"; -export { default as PasswordPrompt } from "./prompts/password"; -export { default as Prompt, State } from "./prompts/prompt"; -export { default as SelectPrompt } from "./prompts/select"; -export { default as SelectKeyPrompt } from "./prompts/select-key"; -export { default as TextPrompt } from "./prompts/text"; -export { block, isCancel, strLength, setGlobalAliases } from "./utils"; +export { default as ConfirmPrompt } from './prompts/confirm'; +export { default as GroupMultiSelectPrompt } from './prompts/group-multiselect'; +export { default as MultiSelectPrompt } from './prompts/multi-select'; +export { default as PasswordPrompt } from './prompts/password'; +export { default as Prompt } from './prompts/prompt'; +export { default as SelectPrompt } from './prompts/select'; +export { default as SelectKeyPrompt } from './prompts/select-key'; +export { default as TextPrompt } from './prompts/text'; +export type { ClackState as State } from './types'; +export { block, isCancel, strLength, setGlobalAliases } from './utils'; diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 96b0501d..bf1c6fe6 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -20,8 +20,6 @@ export interface PromptOptions { debug?: boolean; } -export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error'; - export type LineOption = 'firstLine' | 'newLine' | 'lastLine'; export interface FormatLineOptions { @@ -313,8 +311,8 @@ export default class Prompt { ): NonNullable => { return ( key === 'style' - ? options?.[line]?.[key] ?? options?.default?.[key] ?? ((line) => line) - : options?.[line]?.[key] ?? options?.[line]?.sides ?? options?.default?.[key] ?? '' + ? (options?.[line]?.[key] ?? options?.default?.[key] ?? ((line) => line)) + : (options?.[line]?.[key] ?? options?.[line]?.sides ?? options?.default?.[key] ?? '') ) as NonNullable; }; const getLineOptions = (line: LineOption): Omit => { @@ -376,14 +374,14 @@ export default class Prompt { ): FormatLineOptions[TPosition] => { return ( i === 0 && ar.length === 1 - ? options?.firstLine?.[position] ?? - options?.lastLine?.[position] ?? - firstLine[position] + ? (options?.firstLine?.[position] ?? + options?.lastLine?.[position] ?? + firstLine[position]) : i === 0 - ? firstLine[position] - : i + 1 === ar.length - ? lastLine[position] - : newLine[position] + ? firstLine[position] + : i + 1 === ar.length + ? lastLine[position] + : newLine[position] ) as FormatLineOptions[TPosition]; }; const startLine = opt('start'); @@ -401,9 +399,10 @@ export default class Prompt { .join('\n'); } - private _prevFrame = ''; private render() { - const frame = wrap(this._render(this) ?? '', process.stdout.columns, { hard: true }); + const frame = wrap(this._render(this) ?? '', process.stdout.columns, { + hard: true, + }); if (frame === this._prevFrame) return; if (this.state === 'initial') { diff --git a/packages/core/src/utils/index.ts b/packages/core/src/utils/index.ts index d804b3aa..64a5a353 100644 --- a/packages/core/src/utils/index.ts +++ b/packages/core/src/utils/index.ts @@ -101,12 +101,12 @@ export function strLength(str: string): number { } // Remove ANSI escape codes from the input string. - str = stripAnsi(str); + const stripedStr = stripAnsi(str); let length = 0; - for (let i = 0; i < str.length; i++) { - const code = str.codePointAt(i); + for (let i = 0; i < stripedStr.length; i++) { + const code = stripedStr.codePointAt(i); if (!code || isControlCharacter(code) || isCombiningCharacter(code)) { continue; diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 790e663d..e370f662 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -11,52 +11,52 @@ import { block, isCancel, strLength, -} from "@clack/core"; -import isUnicodeSupported from "is-unicode-supported"; -import color from "picocolors"; -import { cursor, erase } from "sisteransi"; +} from '@clack/core'; +import isUnicodeSupported from 'is-unicode-supported'; +import color from 'picocolors'; +import { cursor, erase } from 'sisteransi'; -export { isCancel, setGlobalAliases } from "@clack/core"; +export { isCancel, setGlobalAliases } from '@clack/core'; const unicode = isUnicodeSupported(); const s = (c: string, fallback: string) => (unicode ? c : fallback); -const S_STEP_ACTIVE = s("◆", "*"); -const S_STEP_CANCEL = s("■", "x"); -const S_STEP_ERROR = s("▲", "x"); -const S_STEP_SUBMIT = s("◇", "o"); +const S_STEP_ACTIVE = s('◆', '*'); +const S_STEP_CANCEL = s('■', 'x'); +const S_STEP_ERROR = s('▲', 'x'); +const S_STEP_SUBMIT = s('◇', 'o'); -const S_BAR_START = s("┌", "T"); -const S_BAR = s("│", "|"); -const S_BAR_END = s("└", "—"); +const S_BAR_START = s('┌', 'T'); +const S_BAR = s('│', '|'); +const S_BAR_END = s('└', '—'); -const S_RADIO_ACTIVE = s("●", ">"); -const S_RADIO_INACTIVE = s("○", " "); -const S_CHECKBOX_ACTIVE = s("◻", "[•]"); -const S_CHECKBOX_SELECTED = s("◼", "[+]"); -const S_CHECKBOX_INACTIVE = s("◻", "[ ]"); -const S_PASSWORD_MASK = s("▪", "•"); +const S_RADIO_ACTIVE = s('●', '>'); +const S_RADIO_INACTIVE = s('○', ' '); +const S_CHECKBOX_ACTIVE = s('◻', '[•]'); +const S_CHECKBOX_SELECTED = s('◼', '[+]'); +const S_CHECKBOX_INACTIVE = s('◻', '[ ]'); +const S_PASSWORD_MASK = s('▪', '•'); -const S_BAR_H = s("─", "-"); -const S_CORNER_TOP_RIGHT = s("╮", "+"); -const S_CONNECT_LEFT = s("├", "+"); -const S_CORNER_BOTTOM_RIGHT = s("╯", "+"); +const S_BAR_H = s('─', '-'); +const S_CORNER_TOP_RIGHT = s('╮', '+'); +const S_CONNECT_LEFT = s('├', '+'); +const S_CORNER_BOTTOM_RIGHT = s('╯', '+'); -const S_INFO = s("●", "•"); -const S_SUCCESS = s("◆", "*"); -const S_WARN = s("▲", "!"); -const S_ERROR = s("■", "x"); +const S_INFO = s('●', '•'); +const S_SUCCESS = s('◆', '*'); +const S_WARN = s('▲', '!'); +const S_ERROR = s('■', 'x'); const symbol = (state: State) => { switch (state) { - case "initial": - case "active": + case 'initial': + case 'active': return color.cyan(S_STEP_ACTIVE); - case "cancel": + case 'cancel': return color.red(S_STEP_CANCEL); - case "error": + case 'error': return color.yellow(S_STEP_ERROR); - case "submit": + case 'submit': return color.green(S_STEP_SUBMIT); } }; @@ -70,9 +70,7 @@ interface LimitOptionsParams { style: (option: TOption, active: boolean) => string; } -const limitOptions = ( - params: LimitOptionsParams -): string[] => { +const limitOptions = (params: LimitOptionsParams): string[] => { const { cursor, options, style } = params; const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; @@ -82,19 +80,14 @@ const limitOptions = ( let slidingWindowLocation = 0; if (cursor >= slidingWindowLocation + maxItems - 3) { - slidingWindowLocation = Math.max( - Math.min(cursor - maxItems + 3, options.length - maxItems), - 0 - ); + slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); } else if (cursor < slidingWindowLocation + 2) { slidingWindowLocation = Math.max(cursor - 2, 0); } - const shouldRenderTopEllipsis = - maxItems < options.length && slidingWindowLocation > 0; + const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; const shouldRenderBottomEllipsis = - maxItems < options.length && - slidingWindowLocation + maxItems < options.length; + maxItems < options.length && slidingWindowLocation + maxItems < options.length; return options .slice(slidingWindowLocation, slidingWindowLocation + maxItems) @@ -102,13 +95,13 @@ const limitOptions = ( const isTopLimit = i === 0 && shouldRenderTopEllipsis; const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; return isTopLimit || isBottomLimit - ? color.dim("...") + ? color.dim('...') : style(option, i + slidingWindowLocation === cursor); }); }; interface ThemeParams { - ctx: Omit; + ctx: Omit; message: string; value: string; valueWithCursor: string | undefined; @@ -129,16 +122,16 @@ function applyTheme(data: ThemeParams): string { start: color.gray(S_BAR), }, }), - ].join("\n"); + ].join('\n'); const placeholder = data.placeholder ? color.inverse(data.placeholder[0]) + color.dim(data.placeholder.slice(1)) - : color.inverse(color.hidden("_")); + : color.inverse(color.hidden('_')); - const value = data.value ?? ""; + const value = data.value ?? ''; switch (ctx.state) { - case "cancel": + case 'cancel': return [ title, format(value, { @@ -147,9 +140,11 @@ function applyTheme(data: ThemeParams): string { style: (line) => color.strikethrough(color.dim(line)), }, }), - ].join("\n"); + ] + .filter(Boolean) + .join('\n'); - case "error": + case 'error': return [ title, format(value, { @@ -167,9 +162,9 @@ function applyTheme(data: ThemeParams): string { start: color.yellow(S_BAR_END), }, }), - ].join("\n"); + ].join('\n'); - case "submit": + case 'submit': return [ title, format(value, { @@ -178,7 +173,7 @@ function applyTheme(data: ThemeParams): string { style: color.dim, }, }), - ].join("\n"); + ].join('\n'); default: return [ @@ -191,18 +186,13 @@ function applyTheme(data: ThemeParams): string { start: color.cyan(S_BAR), }, }), - format( - data.placeholder && !data.value - ? placeholder - : data.valueWithCursor ?? value, - { - default: { - start: color.cyan(S_BAR), - }, - } - ), + format(data.placeholder && !data.value ? placeholder : (data.valueWithCursor ?? value), { + default: { + start: color.cyan(S_BAR), + }, + }), color.cyan(S_BAR_END), - ].join("\n"); + ].join('\n'); } } @@ -258,8 +248,8 @@ export interface ConfirmOptions { initialValue?: boolean; } export const confirm = (opts: ConfirmOptions) => { - const active = opts.active ?? "Yes"; - const inactive = opts.inactive ?? "No"; + const active = opts.active ?? 'Yes'; + const inactive = opts.inactive ?? 'No'; return new ConfirmPrompt({ active, inactive, @@ -274,14 +264,11 @@ export const confirm = (opts: ConfirmOptions) => { ctx: this, message: opts.message, value: - this.state === "submit" || this.state === "cancel" + this.state === 'submit' || this.state === 'cancel' ? this.value ? active : inactive - : `${opt(!!this.value, active)} ${color.dim("/")} ${opt( - !this.value, - inactive - )}`, + : `${opt(!!this.value, active)} ${color.dim('/')} ${opt(!this.value, inactive)}`, valueWithCursor: undefined, }); }, @@ -302,19 +289,16 @@ export interface SelectOptions { } export const select = (opts: SelectOptions) => { - const opt = ( - option: Option, - state: "inactive" | "active" | "selected" | "cancelled" - ) => { + const opt = (option: Option, state: 'inactive' | 'active' | 'selected' | 'cancelled') => { const label = option.label ?? String(option.value); switch (state) { - case "selected": + case 'selected': return `${color.dim(label)}`; - case "active": + case 'active': return `${color.green(S_RADIO_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : "" + option.hint ? color.dim(`(${option.hint})`) : '' }`; - case "cancelled": + case 'cancelled': return `${color.strikethrough(color.dim(label))}`; default: return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; @@ -327,19 +311,19 @@ export const select = (opts: SelectOptions) => { render() { let value: string; switch (this.state) { - case "submit": - value = opt(this.options[this.cursor], "selected"); + case 'submit': + value = opt(this.options[this.cursor], 'selected'); break; - case "cancel": - value = opt(this.options[this.cursor], "cancelled"); + case 'cancel': + value = opt(this.options[this.cursor], 'cancelled'); break; default: { value = limitOptions({ cursor: this.cursor, options: this.options, maxItems: opts.maxItems, - style: (item, active) => opt(item, active ? "active" : "inactive"), - }).join("\n"); + style: (item, active) => opt(item, active ? 'active' : 'inactive'), + }).join('\n'); break; } } @@ -356,50 +340,46 @@ export const select = (opts: SelectOptions) => { export const selectKey = (opts: SelectOptions) => { const opt = ( option: Option, - state: "inactive" | "active" | "selected" | "cancelled" = "inactive" + state: 'inactive' | 'active' | 'selected' | 'cancelled' = 'inactive' ) => { const label = option.label ?? String(option.value); - if (state === "selected") { + if (state === 'selected') { return `${color.dim(label)}`; } - if (state === "cancelled") { + if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; } - if (state === "active") { + if (state === 'active') { return `${color.bgCyan(color.gray(` ${option.value} `))} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : "" + option.hint ? color.dim(`(${option.hint})`) : '' }`; } return `${color.gray( color.bgWhite(color.inverse(` ${option.value} `)) - )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ""}`; + )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`; }; return new SelectKeyPrompt({ options: opts.options, initialValue: opts.initialValue, render() { - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ - opts.message - }\n`; + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}`; switch (this.state) { - case "submit": - return `${title}${color.gray(S_BAR)} ${opt( + case 'submit': + return `${title}\n${color.gray(S_BAR)} ${opt( // biome-ignore lint/style/noNonNullAssertion: this.options.find((opt) => opt.value === this.value)!, - "selected" + 'selected' )}`; - case "cancel": - return `${title}${color.gray(S_BAR)} ${opt( + case 'cancel': + return `${title}\n${color.gray(S_BAR)} ${opt( this.options[0], - "cancelled" + 'cancelled' )}\n${color.gray(S_BAR)}`; default: - return `${title}${color.cyan(S_BAR)} ${this.options - .map((option, i) => - opt(option, i === this.cursor ? "active" : "inactive") - ) + return `${title}\n${color.cyan(S_BAR)} ${this.options + .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } }, @@ -417,32 +397,26 @@ export interface MultiSelectOptions { export const multiselect = (opts: MultiSelectOptions) => { const opt = ( option: Option, - state: - | "inactive" - | "active" - | "selected" - | "active-selected" - | "submitted" - | "cancelled" + state: 'inactive' | 'active' | 'selected' | 'active-selected' | 'submitted' | 'cancelled' ) => { const label = option.label ?? String(option.value); - if (state === "active") { + if (state === 'active') { return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : "" + option.hint ? color.dim(`(${option.hint})`) : '' }`; } - if (state === "selected") { + if (state === 'selected') { return `${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } - if (state === "cancelled") { + if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; } - if (state === "active-selected") { + if (state === 'active-selected') { return `${color.green(S_CHECKBOX_SELECTED)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : "" + option.hint ? color.dim(`(${option.hint})`) : '' }`; } - if (state === "submitted") { + if (state === 'submitted') { return `${color.dim(label)}`; } return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; @@ -457,10 +431,8 @@ export const multiselect = (opts: MultiSelectOptions) => { if (this.required && selected.length === 0) return `Please select at least one option.\n${color.reset( color.dim( - `Press ${color.gray( - color.bgWhite(color.inverse(" space ")) - )} to select, ${color.gray( - color.bgWhite(color.inverse(" enter ")) + `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( + color.bgWhite(color.inverse(' enter ')) )} to submit` ) )}`; @@ -472,43 +444,43 @@ export const multiselect = (opts: MultiSelectOptions) => { const styleOption = (option: Option, active: boolean) => { const selected = this.value.includes(option.value); if (active && selected) { - return opt(option, "active-selected"); + return opt(option, 'active-selected'); } if (selected) { - return opt(option, "selected"); + return opt(option, 'selected'); } - return opt(option, active ? "active" : "inactive"); + return opt(option, active ? 'active' : 'inactive'); }; switch (this.state) { - case "submit": { + case 'submit': { value = this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, "submitted")) - .join(color.dim(", ")) || color.dim("none"); + .map((option) => opt(option, 'submitted')) + .join(color.dim(', ')) || color.dim('none'); break; } - case "cancel": { + case 'cancel': { value = this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, "cancelled")) - .join(color.dim(", ")) ?? ""; + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', ')) ?? ''; break; } - case "error": { + case 'error': { error = format( this.error - .split("\n") + .split('\n') .map((ln, i) => (i === 0 ? color.yellow(ln) : ln)) - .join("\n"), + .join('\n'), { firstLine: { start: color.yellow(S_BAR_END), }, default: { - start: color.hidden("-"), + start: color.hidden('-'), }, } ); @@ -517,7 +489,7 @@ export const multiselect = (opts: MultiSelectOptions) => { maxItems: opts.maxItems, options: this.options, style: styleOption, - }).join("\n"); + }).join('\n'); break; } default: { @@ -526,7 +498,7 @@ export const multiselect = (opts: MultiSelectOptions) => { maxItems: opts.maxItems, options: this.options, style: styleOption, - }).join("\n"); + }).join('\n'); break; } } @@ -548,59 +520,52 @@ export interface GroupMultiSelectOptions { required?: boolean; cursorAt?: Value; } -export const groupMultiselect = ( - opts: GroupMultiSelectOptions -) => { +export const groupMultiselect = (opts: GroupMultiSelectOptions) => { const opt = ( option: Option, state: - | "inactive" - | "active" - | "selected" - | "active-selected" - | "group-active" - | "group-active-selected" - | "submitted" - | "cancelled", + | 'inactive' + | 'active' + | 'selected' + | 'active-selected' + | 'group-active' + | 'group-active-selected' + | 'submitted' + | 'cancelled', options: Option[] = [] ) => { const label = option.label ?? String(option.value); - const isItem = typeof (option as any).group === "string"; - const next = - isItem && (options[options.indexOf(option) + 1] ?? { group: true }); + const isItem = typeof (option as any).group === 'string'; + const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); const isLast = isItem && (next as any).group === true; - const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ""; + const prefix = isItem ? `${isLast ? S_BAR_END : S_BAR} ` : ''; - if (state === "active") { + if (state === 'active') { return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ - option.hint ? color.dim(`(${option.hint})`) : "" + option.hint ? color.dim(`(${option.hint})`) : '' }`; } - if (state === "group-active") { + if (state === 'group-active') { return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; } - if (state === "group-active-selected") { + if (state === 'group-active-selected') { return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } - if (state === "selected") { - return `${color.dim(prefix)}${color.green( - S_CHECKBOX_SELECTED - )} ${color.dim(label)}`; + if (state === 'selected') { + return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; } - if (state === "cancelled") { + if (state === 'cancelled') { return `${color.strikethrough(color.dim(label))}`; } - if (state === "active-selected") { + if (state === 'active-selected') { return `${color.dim(prefix)}${color.green( S_CHECKBOX_SELECTED - )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ""}`; + )} ${label} ${option.hint ? color.dim(`(${option.hint})`) : ''}`; } - if (state === "submitted") { + if (state === 'submitted') { return `${color.dim(label)}`; } - return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim( - label - )}`; + return `${color.dim(prefix)}${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; }; return new GroupMultiSelectPrompt({ @@ -612,10 +577,8 @@ export const groupMultiselect = ( if (this.required && selected.length === 0) return `Please select at least one option.\n${color.reset( color.dim( - `Press ${color.gray( - color.bgWhite(color.inverse(" space ")) - )} to select, ${color.gray( - color.bgWhite(color.inverse(" enter ")) + `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( + color.bgWhite(color.inverse(' enter ')) )} to submit` ) )}`; @@ -623,72 +586,63 @@ export const groupMultiselect = ( render() { const symbol = (state: State) => { switch (state) { - case "initial": - case "active": + case 'initial': + case 'active': return color.cyan(S_STEP_ACTIVE); - case "cancel": + case 'cancel': return color.red(S_STEP_CANCEL); - case "error": + case 'error': return color.yellow(S_STEP_ERROR); - case "submit": + case 'submit': return color.green(S_STEP_SUBMIT); } }; - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${ - opts.message - }\n`; + const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}`; switch (this.state) { - case "submit": { + case 'submit': { return `${title}\n${color.gray(S_BAR)} ${this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, "submitted")) - .join(color.dim(", "))}`; + .map((option) => opt(option, 'submitted')) + .join(color.dim(', '))}`; } - case "cancel": { + case 'cancel': { const label = this.options .filter(({ value }) => this.value.includes(value)) - .map((option) => opt(option, "cancelled")) - .join(color.dim(", ")); + .map((option) => opt(option, 'cancelled')) + .join(color.dim(', ')); return `${title}\n${color.gray(S_BAR)} ${ - label.trim() ? `${label}\n${color.gray(S_BAR)}` : "" + label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' }`; } - case "error": { + case 'error': { const footer = this.error - .split("\n") + .split('\n') .map((ln, i) => - i === 0 - ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` - : ` ${ln}` + i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` ) - .join("\n"); + .join('\n'); return `${title}\n${color.yellow(S_BAR)} ${this.options .map((option, i, options) => { const selected = this.value.includes(option.value) || - (option.group === true && - this.isGroupSelected(`${option.value}`)); + (option.group === true && this.isGroupSelected(`${option.value}`)); const active = i === this.cursor; const groupActive = !active && - typeof option.group === "string" && + typeof option.group === 'string' && this.options[this.cursor].value === option.group; if (groupActive) { - return opt( - option, - selected ? "group-active-selected" : "group-active", - options - ); + return opt(option, selected ? 'group-active-selected' : 'group-active', options); } if (active && selected) { - return opt(option, "active-selected", options); + return opt(option, 'active-selected', options); } if (selected) { - return opt(option, "selected", options); + return opt(option, 'selected', options); } - return opt(option, active ? "active" : "inactive", options); + return opt(option, active ? 'active' : 'inactive', options); }) .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; } @@ -697,27 +651,22 @@ export const groupMultiselect = ( .map((option, i, options) => { const selected = this.value.includes(option.value) || - (option.group === true && - this.isGroupSelected(`${option.value}`)); + (option.group === true && this.isGroupSelected(`${option.value}`)); const active = i === this.cursor; const groupActive = !active && - typeof option.group === "string" && + typeof option.group === 'string' && this.options[this.cursor].value === option.group; if (groupActive) { - return opt( - option, - selected ? "group-active-selected" : "group-active", - options - ); + return opt(option, selected ? 'group-active-selected' : 'group-active', options); } if (active && selected) { - return opt(option, "active-selected", options); + return opt(option, 'active-selected', options); } if (selected) { - return opt(option, "selected", options); + return opt(option, 'selected', options); } - return opt(option, active ? "active" : "inactive", options); + return opt(option, active ? 'active' : 'inactive', options); }) .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; } @@ -726,7 +675,7 @@ export const groupMultiselect = ( }).prompt() as Promise; }; -export const note = (message = "", title = "") => { +export const note = (message = '', title = '') => { const maxWidth = Math.floor((process.stdout.columns ?? 80) * 0.8); const lines = format(message, { default: { @@ -745,23 +694,23 @@ export const note = (message = "", title = "") => { `${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( S_BAR_H.repeat(Math.max(len - titleLen - 3, 0)) + S_CORNER_TOP_RIGHT )}`, - color.gray(S_BAR + " ".repeat(len) + S_BAR), + color.gray(S_BAR + ' '.repeat(len) + S_BAR), lines .map( (line) => line + - " ".repeat(Math.max(len + (unicode ? 2 : 1) - strLength(line), 0)) + + ' '.repeat(Math.max(len + (unicode ? 2 : 1) - strLength(line), 0)) + color.gray(S_BAR) ) - .join("\n"), - color.gray(S_BAR + " ".repeat(len) + S_BAR), + .join('\n'), + color.gray(S_BAR + ' '.repeat(len) + S_BAR), color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len) + S_CORNER_BOTTOM_RIGHT), - "", - ].join("\n"); + '', + ].join('\n'); process.stdout.write(noteBox); }; -export const cancel = (message = "") => { +export const cancel = (message = '') => { process.stdout.write( `${format(message, { default: { @@ -775,7 +724,7 @@ export const cancel = (message = "") => { ); }; -export const intro = (title = "") => { +export const intro = (title = '') => { process.stdout.write( `${format(title, { firstLine: { @@ -788,7 +737,7 @@ export const intro = (title = "") => { ); }; -export const outro = (message = "") => { +export const outro = (message = '') => { process.stdout.write( [ color.gray(S_BAR), @@ -800,9 +749,9 @@ export const outro = (message = "") => { start: color.gray(S_BAR_END), }, }), - "", - "", - ].join("\n") + '', + '', + ].join('\n') ); }; @@ -810,10 +759,7 @@ export type LogMessageOptions = { symbol?: string; }; export const log = { - message: ( - message = "", - { symbol = color.gray(S_BAR) }: LogMessageOptions = {} - ) => { + message: (message = '', { symbol = color.gray(S_BAR) }: LogMessageOptions = {}) => { process.stdout.write( `${format(message, { firstLine: { @@ -857,14 +803,14 @@ export const log = { }; export const spinner = () => { - const frames = unicode ? ["◒", "◐", "◓", "◑"] : ["•", "o", "O", "0"]; + const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; const delay = unicode ? 80 : 120; - const isCI = process.env.CI === "true"; + const isCI = process.env.CI === 'true'; let unblock: () => void; let loop: NodeJS.Timeout; let isSpinnerActive = false; - let _message = ""; + let _message = ''; let _prevMessage: string | undefined = undefined; const formatMessage = (symbol: string, msg: string): string => { @@ -880,14 +826,14 @@ export const spinner = () => { const clearPrevMessage = (): void => { if (_prevMessage === undefined) return; - if (isCI) process.stdout.write("\n"); + if (isCI) process.stdout.write('\n'); const linesCounter = _prevMessage.split(/\n/g).length; process.stdout.write(cursor.move(-999, (linesCounter - 1) * -1)); process.stdout.write(erase.down(linesCounter)); }; const handleExit = (code: number) => { - const msg = code > 1 ? "Something went wrong" : "Canceled"; + const msg = code > 1 ? 'Something went wrong' : 'Canceled'; if (isSpinnerActive) stop(msg, code); }; @@ -896,28 +842,28 @@ export const spinner = () => { const registerHooks = () => { // Reference: https://nodejs.org/api/process.html#event-uncaughtexception - process.on("uncaughtExceptionMonitor", errorEventHandler); + process.on('uncaughtExceptionMonitor', errorEventHandler); // Reference: https://nodejs.org/api/process.html#event-unhandledrejection - process.on("unhandledRejection", errorEventHandler); + process.on('unhandledRejection', errorEventHandler); // Reference Signal Events: https://nodejs.org/api/process.html#signal-events - process.on("SIGINT", signalEventHandler); - process.on("SIGTERM", signalEventHandler); - process.on("exit", handleExit); + process.on('SIGINT', signalEventHandler); + process.on('SIGTERM', signalEventHandler); + process.on('exit', handleExit); }; const clearHooks = () => { - process.removeListener("uncaughtExceptionMonitor", errorEventHandler); - process.removeListener("unhandledRejection", errorEventHandler); - process.removeListener("SIGINT", signalEventHandler); - process.removeListener("SIGTERM", signalEventHandler); - process.removeListener("exit", handleExit); + process.removeListener('uncaughtExceptionMonitor', errorEventHandler); + process.removeListener('unhandledRejection', errorEventHandler); + process.removeListener('SIGINT', signalEventHandler); + process.removeListener('SIGTERM', signalEventHandler); + process.removeListener('exit', handleExit); }; const parseMessage = (msg: string): string => { - return msg.replace(/\.+$/, ""); + return msg.replace(/\.+$/, ''); }; - const start = (msg = ""): void => { + const start = (msg = ''): void => { isSpinnerActive = true; unblock = block(); _message = parseMessage(msg); @@ -932,9 +878,7 @@ export const spinner = () => { clearPrevMessage(); _prevMessage = _message; const frame = color.magenta(frames[frameIndex]); - const loadingDots = isCI - ? "..." - : ".".repeat(Math.floor(dotsTimer)).slice(0, 3); + const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); _prevMessage = _message; process.stdout.write(formatMessage(frame, _message + loadingDots)); frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; @@ -942,7 +886,7 @@ export const spinner = () => { }, delay); }; - const stop = (msg = "", code = 0): void => { + const stop = (msg = '', code = 0): void => { isSpinnerActive = false; clearInterval(loop); clearPrevMessage(); @@ -950,15 +894,15 @@ export const spinner = () => { code === 0 ? color.green(S_STEP_SUBMIT) : code === 1 - ? color.red(S_STEP_CANCEL) - : color.red(S_STEP_ERROR); + ? color.red(S_STEP_CANCEL) + : color.red(S_STEP_ERROR); _message = parseMessage(msg ?? _message); process.stdout.write(`${step} ${_message}\n`); clearHooks(); unblock(); }; - const message = (msg = ""): void => { + const message = (msg = ''): void => { _message = parseMessage(msg || _message); }; @@ -1013,8 +957,8 @@ export const group = async ( // Pass the results to the onCancel function // so the user can decide what to do with the results // TODO: Switch to callback within core to avoid isCancel Fn - if (typeof opts?.onCancel === "function" && isCancel(result)) { - results[name] = "canceled"; + if (typeof opts?.onCancel === 'function' && isCancel(result)) { + results[name] = 'canceled'; opts.onCancel({ results }); continue; } @@ -1033,9 +977,7 @@ export type Task = { /** * Task function */ - task: ( - message: (string: string) => void - ) => string | Promise | void | Promise; + task: (message: (string: string) => void) => string | Promise | void | Promise; /** * If enabled === false the task will be skipped diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 571757f7..6113bdee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,14 +7,6 @@ settings: importers: .: - specifiers: - '@changesets/cli': ^2.26.0 - '@types/node': '18' - merge2: ^1.4.1 - organize-imports-cli: ^0.10.0 - prettier: ^2.8.4 - typescript: ^4.9.5 - unbuild: 1.1.2 devDependencies: '@biomejs/biome': specifier: 1.9.4 @@ -33,36 +25,38 @@ importers: version: 2.0.0(typescript@5.2.2) examples/basic: - specifiers: - '@clack/core': workspace:* - '@clack/prompts': workspace:* - jiti: ^1.17.0 - picocolors: ^1.0.0 - dependencies: - '@clack/core': link:../../packages/core - '@clack/prompts': link:../../packages/prompts - picocolors: 1.0.0 + dependencies: + '@clack/core': + specifier: workspace:* + version: link:../../packages/core + '@clack/prompts': + specifier: workspace:* + version: link:../../packages/prompts + picocolors: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: - jiti: 1.19.3 + jiti: + specifier: ^1.17.0 + version: 1.17.0 examples/changesets: - specifiers: - '@clack/core': workspace:* - '@clack/prompts': workspace:* - jiti: ^1.17.0 - picocolors: ^1.0.0 - dependencies: - '@clack/core': link:../../packages/core - '@clack/prompts': link:../../packages/prompts - picocolors: 1.0.0 + dependencies: + '@clack/core': + specifier: workspace:* + version: link:../../packages/core + '@clack/prompts': + specifier: workspace:* + version: link:../../packages/prompts + picocolors: + specifier: ^1.0.0 + version: 1.0.0 devDependencies: - jiti: 1.19.3 + jiti: + specifier: ^1.17.0 + version: 1.17.0 packages/core: - specifiers: - picocolors: ^1.0.0 - sisteransi: ^1.0.5 - wrap-ansi: ^8.1.0 dependencies: picocolors: specifier: ^1.0.0 @@ -79,11 +73,6 @@ importers: version: 8.1.0 packages/prompts: - specifiers: - '@clack/core': ^0.3.3 - is-unicode-supported: ^1.3.0 - picocolors: ^1.0.0 - sisteransi: ^1.0.5 dependencies: '@clack/core': specifier: workspace:* @@ -95,7 +84,9 @@ importers: specifier: ^1.0.5 version: 1.0.5 devDependencies: - is-unicode-supported: 1.3.0 + is-unicode-supported: + specifier: ^1.3.0 + version: 1.3.0 packages: