Skip to content

Commit

Permalink
chore: rebase branch
Browse files Browse the repository at this point in the history
  • Loading branch information
orochaa committed Dec 15, 2024
1 parent 69b7030 commit f9cae47
Show file tree
Hide file tree
Showing 6 changed files with 346 additions and 383 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
"volta": {
"node": "20.18.1"
}
}
}
3 changes: 2 additions & 1 deletion packages/core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,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 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";
189 changes: 109 additions & 80 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,24 @@
import { stdin, stdout } from 'node:process';
import readline, { type Key, type ReadLine } from 'node:readline';
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';

import type { ClackEvents, ClackState, InferSetType } from '../types';
import { stdin, stdout } from "node:process";
import readline, { type Key, type ReadLine } from "node:readline";
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";

import type { ClackEvents, ClackState, InferSetType } from "../types";

export interface PromptOptions<Self extends Prompt> {
render(this: Omit<Self, 'prompt'>): string | undefined;
render(this: Omit<Self, "prompt">): string | undefined;
placeholder?: string;
initialValue?: any;
validate?: ((value: any) => string | undefined) | undefined;
Expand All @@ -20,9 +27,7 @@ export interface PromptOptions<Self extends Prompt> {
debug?: boolean;
}

export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error';

export type LineOption = 'firstLine' | 'newLine' | 'lastLine';
export type LineOption = "firstLine" | "newLine" | "lastLine";

export interface FormatLineOptions {
/**
Expand Down Expand Up @@ -71,7 +76,8 @@ export interface FormatLineOptions {
style: (line: string) => string;
}

export interface FormatOptions extends Record<LineOption, Partial<FormatLineOptions>> {
export interface FormatOptions
extends Record<LineOption, Partial<FormatLineOptions>> {
/**
* Shorthand to define values for each line
* @example
Expand Down Expand Up @@ -109,15 +115,18 @@ export default class Prompt {
protected output: Writable;

private rl!: ReadLine;
private opts: Omit<PromptOptions<Prompt>, 'render' | 'input' | 'output'>;
private _render: (context: Omit<Prompt, 'prompt'>) => string | undefined;
private opts: Omit<PromptOptions<Prompt>, "render" | "input" | "output">;
private _render: (context: Omit<Prompt, "prompt">) => string | undefined;
private _track = false;
private _prevFrame = '';
private _subscribers = new Map<string, { cb: (...args: any) => any; once?: boolean }[]>();
private _prevFrame = "";
private _subscribers = new Map<
string,
{ cb: (...args: any) => any; once?: boolean }[]
>();
protected _cursor = 0;

public state: ClackState = 'initial';
public error = '';
public state: ClackState = "initial";
public error = "";
public value: any;

constructor(options: PromptOptions<Prompt>, trackValue = true) {
Expand Down Expand Up @@ -177,7 +186,10 @@ export default class Prompt {
* @param event - The event name
* @param data - The data to pass to the callback
*/
public emit<T extends keyof ClackEvents>(event: T, ...data: Parameters<ClackEvents[T]>) {
public emit<T extends keyof ClackEvents>(
event: T,
...data: Parameters<ClackEvents[T]>
) {
const cbs = this._subscribers.get(event) ?? [];
const cleanup: (() => void)[] = [];

Expand All @@ -199,9 +211,9 @@ export default class Prompt {
const sink = new WriteStream(0);
sink._write = (chunk, encoding, done) => {
if (this._track) {
this.value = this.rl.line.replace(/\t/g, '');
this.value = this.rl.line.replace(/\t/g, "");
this._cursor = this.rl.cursor;
this.emit('value', this.value);
this.emit("value", this.value);
}
done();
};
Expand All @@ -211,7 +223,7 @@ export default class Prompt {
input: this.input,
output: sink,
tabSize: 2,
prompt: '',
prompt: "",
escapeCodeTimeout: 50,
});
readline.emitKeypressEvents(this.input, this.rl);
Expand All @@ -220,80 +232,80 @@ export default class Prompt {
this.rl.write(this.opts.initialValue);
}

this.input.on('keypress', this.onKeypress);
this.input.on("keypress", this.onKeypress);
setRawMode(this.input, true);
this.output.on('resize', this.render);
this.output.on("resize", this.render);

this.render();

this.once('submit', () => {
this.once("submit", () => {
this.output.write(cursor.show);
this.output.off('resize', this.render);
this.output.off("resize", this.render);
setRawMode(this.input, false);
resolve(this.value);
});
this.once('cancel', () => {
this.once("cancel", () => {
this.output.write(cursor.show);
this.output.off('resize', this.render);
this.output.off("resize", this.render);
setRawMode(this.input, false);
resolve(CANCEL_SYMBOL);
});
});
}

private onKeypress(char: string, key?: Key) {
if (this.state === 'error') {
this.state = 'active';
if (this.state === "error") {
this.state = "active";
}
if (key?.name && !this._track && ALIASES.has(key.name)) {
this.emit('cursor', ALIASES.get(key.name));
this.emit("cursor", ALIASES.get(key.name));
}
if (key?.name && KEYS.has(key.name as InferSetType<typeof KEYS>)) {
this.emit('cursor', key.name as InferSetType<typeof KEYS>);
this.emit("cursor", key.name as InferSetType<typeof KEYS>);
}
if (char && (char.toLowerCase() === 'y' || char.toLowerCase() === 'n')) {
this.emit('confirm', char.toLowerCase() === 'y');
if (char && (char.toLowerCase() === "y" || char.toLowerCase() === "n")) {
this.emit("confirm", char.toLowerCase() === "y");
}
if (char === '\t' && this.opts.placeholder) {
if (char === "\t" && this.opts.placeholder) {
if (!this.value) {
this.rl.write(this.opts.placeholder);
this.emit('value', this.opts.placeholder);
this.emit("value", this.opts.placeholder);
}
}
if (char) {
this.emit('key', char.toLowerCase());
this.emit("key", char.toLowerCase());
}

if (key?.name === 'return') {
if (key?.name === "return") {
if (this.opts.validate) {
const problem = this.opts.validate(this.value);
if (problem) {
this.error = problem;
this.state = 'error';
this.state = "error";
this.rl.write(this.value);
}
}
if (this.state !== 'error') {
this.state = 'submit';
if (this.state !== "error") {
this.state = "submit";
}
}

if (hasAliasKey([char, key?.name, key?.sequence], 'cancel')) {
this.state = 'cancel';
if (hasAliasKey([char, key?.name, key?.sequence], "cancel")) {
this.state = "cancel";
}
if (this.state === 'submit' || this.state === 'cancel') {
this.emit('finalize');
if (this.state === "submit" || this.state === "cancel") {
this.emit("finalize");
}
this.render();
if (this.state === 'submit' || this.state === 'cancel') {
if (this.state === "submit" || this.state === "cancel") {
this.close();
}
}

protected close() {
this.input.unpipe();
this.input.removeListener('keypress', this.onKeypress);
this.output.write('\n');
this.input.removeListener("keypress", this.onKeypress);
this.output.write("\n");
setRawMode(this.input, false);
this.rl.close();
this.emit(`${this.state}`, this.value);
Expand All @@ -302,32 +314,43 @@ export default class Prompt {

private restoreCursor() {
const lines =
wrap(this._prevFrame, process.stdout.columns, { hard: true }).split('\n').length - 1;
wrap(this._prevFrame, process.stdout.columns, { hard: true }).split("\n")
.length - 1;
this.output.write(cursor.move(-999, lines * -1));
}

public format(text: string, options?: Partial<FormatOptions>): string {
const getLineOption = <TLine extends LineOption, TKey extends keyof FormatLineOptions>(
const getLineOption = <
TLine extends LineOption,
TKey extends keyof FormatLineOptions
>(
line: TLine,
key: TKey
): NonNullable<FormatOptions[TLine][TKey]> => {
return (
key === 'style'
? options?.[line]?.[key] ?? options?.default?.[key] ?? ((line) => line)
: options?.[line]?.[key] ?? options?.[line]?.sides ?? options?.default?.[key] ?? ''
key === "style"
? options?.[line]?.[key] ??
options?.default?.[key] ??
((line) => line)
: options?.[line]?.[key] ??
options?.[line]?.sides ??
options?.default?.[key] ??
""
) as NonNullable<FormatOptions[TLine][TKey]>;
};
const getLineOptions = (line: LineOption): Omit<FormatLineOptions, 'sides'> => {
const getLineOptions = (
line: LineOption
): Omit<FormatLineOptions, "sides"> => {
return {
start: getLineOption(line, 'start'),
end: getLineOption(line, 'end'),
style: getLineOption(line, 'style'),
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 firstLine = getLineOptions("firstLine");
const newLine = getLineOptions("newLine");
const lastLine = getLineOptions("lastLine");

const emptySlots =
Math.max(
Expand All @@ -344,7 +367,7 @@ export default class Prompt {

for (const paragraph of paragraphs) {
const words = paragraph.split(/\s/g);
let currentLine = '';
let currentLine = "";

for (const word of words) {
if (strLength(currentLine + word) + emptySlots + 1 <= maxWidth) {
Expand All @@ -371,7 +394,9 @@ export default class Prompt {

return formattedLines
.map((line, i, ar) => {
const opt = <TPosition extends Exclude<keyof FormatLineOptions, 'sides'>>(
const opt = <
TPosition extends Exclude<keyof FormatLineOptions, "sides">
>(
position: TPosition
): FormatLineOptions[TPosition] => {
return (
Expand All @@ -386,27 +411,31 @@ export default class Prompt {
: newLine[position]
) as FormatLineOptions[TPosition];
};
const startLine = opt('start');
const endLine = opt('end');
const styleLine = opt('style');
const startLine = opt("start");
const endLine = opt("end");
const styleLine = opt("style");
// only format the line without the leading space.
const leadingSpaceRegex = /^\s/;
const styledLine = leadingSpaceRegex.test(line)
? ` ${styleLine(line.slice(1))}`
: styleLine(line);
const fullLine =
styledLine + ' '.repeat(Math.max(minWidth - strLength(styledLine) - emptySlots, 0));
return [startLine, fullLine, endLine].join(' ');
styledLine +
" ".repeat(
Math.max(minWidth - strLength(styledLine) - emptySlots, 0)
);
return [startLine, fullLine, endLine].join(" ");
})
.join('\n');
.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') {
if (this.state === "initial") {
this.output.write(cursor.hide);
} else {
const diff = diffLines(this._prevFrame, frame);
Expand All @@ -416,7 +445,7 @@ export default class Prompt {
const diffLine = diff[0];
this.output.write(cursor.move(0, diffLine));
this.output.write(erase.lines(1));
const lines = frame.split('\n');
const lines = frame.split("\n");
this.output.write(lines[diffLine]);
this._prevFrame = frame;
this.output.write(cursor.move(0, lines.length - diffLine - 1));
Expand All @@ -427,9 +456,9 @@ export default class Prompt {
const diffLine = diff[0];
this.output.write(cursor.move(0, diffLine));
this.output.write(erase.down());
const lines = frame.split('\n');
const lines = frame.split("\n");
const newLines = lines.slice(diffLine);
this.output.write(newLines.join('\n'));
this.output.write(newLines.join("\n"));
this._prevFrame = frame;
return;
}
Expand All @@ -438,8 +467,8 @@ export default class Prompt {
}

this.output.write(frame);
if (this.state === 'initial') {
this.state = 'active';
if (this.state === "initial") {
this.state = "active";
}
this._prevFrame = frame;
}
Expand Down
Loading

0 comments on commit f9cae47

Please sign in to comment.