import Prompt from "inquirer/lib/prompts/base"; import observe from "inquirer/lib/utils/events"; import {Answers, MultiTextInputQuestion, Question} from "inquirer"; import {Interface as ReadLineInterface} from "readline"; import {filter} from "rxjs/operators" import chalk from "chalk"; import {Subject} from "rxjs"; import figures from "figures"; import Paginator from "inquirer/lib/utils/paginator"; interface ExtendedReadLine extends ReadLineInterface { line: string cursor: number } declare module "inquirer" { export interface MultiTextInputQuestionOptions { pageSize?: number } export interface MultiTextInputQuestion extends Question, MultiTextInputQuestionOptions { /** @inheritDoc */ type: "multitext" default?: string[] } export interface QuestionMap { ["multitext"]: HierarchicalCheckboxQuestion } } export class MultiTextInput = MultiTextInputQuestion> extends Prompt { constructor(question: QuestionT, readline: ExtendedReadLine, answers: AnswerT) { super(question, readline, answers); this.value = question.default?.slice() || [] this.activeIndex = this.value.length } _run(callback: MultiTextInput["done"]) { this.status = "touched" this.done = callback const events = observe(this.rl); this.rl.on("history", (history) => { history.splice(0, history.length) }) events.normalizedUpKey.subscribe(() => this.onUpKey()) events.normalizedDownKey.subscribe(() => this.onDownKey()) events.line.subscribe((line) => this.onLine(line)) events.keypress.pipe(filter((event) => { return event.key.name === "r" && event.key.ctrl === true })).subscribe(() => this.onReset()) events.keypress.pipe(filter((event) => { return event.key.name === "x" && event.key.ctrl === true })).subscribe(() => this.onClear()) events.keypress.subscribe(() => this.scheduleRender()) const onSubmit = this.handleSubmitEvents(this.submitter) onSubmit.success.subscribe((result) => this.onValidated(result.value)) onSubmit.error.subscribe((result) => this.onValidationError(result.isValid)) this.scheduleRender() } private done: (state: any) => void|null private activeIndex: number private readonly value: string[] private submitter: Subject = new Subject() // @ts-ignore private paginator: Paginator = new Paginator(this.screen, {isInfinite: false}); private scheduledRender: NodeJS.Immediate|null = null private get readline(): ExtendedReadLine { return this.rl } private get isNew(): boolean { return this.activeIndex === this.value.length } private get activeValue(): string { return this.isNew ? "" : this.value[this.activeIndex] } private set activeValue(newValue: string) { if (this.isNew) { this.value.push(newValue) } else { this.value[this.activeIndex] = newValue } } private render(error?: string) { this.scheduledRender = null const outputs: string[] = [this.getQuestion()] const bottomOutputs: string[] = [] if (this.status === "answered") { outputs.push(...this.value.map((s) => chalk.cyan(`${s}`)).join(", ")) } else { if (typeof error === "string") { bottomOutputs.push(`${figures.warning} ${error}`) } else if (this.readline.line === "") { if (this.isNew) { bottomOutputs.push(chalk.dim(`Begin typing to add a new entry. Press ${chalk.yellow("")} to submit these entries, ${chalk.yellow("+x")} to delete all entries, or ${chalk.yellow("")}/${chalk.yellow("")} to change existing entries.`)) } else { bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("")} or ${chalk.yellow("+x")} to delete this entry or ${chalk.yellow("+r")} to revert.`)) } } else { outputs.push(this.readline.line) if (this.isNew) { bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("")} to add this entry or ${chalk.yellow("+r")} or ${chalk.yellow("+x")} to clear it.`)) } else if (this.activeValue !== this.readline.line) { bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("")} to save this entry, ${chalk.yellow("+r")} to revert it, or ${chalk.yellow("+x")} to delete it.`)) } else { bottomOutputs.push(chalk.dim(`Begin typing to change this entry or press ${chalk.yellow("+x")} to delete it. Press ${chalk.yellow("")}/${chalk.yellow("")} to change other entries.`)) } } bottomOutputs.push(this.renderList()) } this.screen.render(outputs.join(""), bottomOutputs.join("\n")) } private renderList(): string { const outputs: string[] = [] outputs.push(...this.value.map((entry, index):string => { const isSelected = index === this.activeIndex return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isSelected ? chalk.cyan(entry) : entry}` })) outputs.push(`${this.isNew ? chalk.cyan(figures.pointer) : " "} ${this.isNew ? chalk.cyan.dim("(New Entry)") : chalk.dim("(New Entry)")}`) // @ts-ignore return this.paginator.paginate(outputs.join("\n"), this.activeIndex, this.opt.pageSize) } private onUpKey() { if (this.readline.line !== this.activeValue) { return } if (this.activeIndex > 0) { this.changeIndex(-1) this.scheduleRender() } } private onDownKey() { if (this.readline.line !== this.activeValue) { return } if (this.activeIndex < this.value.length) { this.changeIndex(+1) this.scheduleRender() } } private onLine(line: string) { if (line === "") { if (this.isNew) { this.submitter.next(this.value.slice()) } else { this.value.splice(this.activeIndex, 1) this.changeIndex(0) } } else { this.activeValue = line this.changeIndex(+1) } this.scheduleRender() } private onReset() { this.changeIndex(0) this.scheduleRender() } private onClear() { if (!this.isNew) { this.value.splice(this.activeIndex, 1) this.changeIndex(0) this.scheduleRender() } else if (this.readline.line !== "") { this.readline.line = "" } else { this.value.splice(0, this.value.length) this.activeIndex = 0 this.changeIndex(0) } this.scheduleRender() } private scheduleRender() { if (this.scheduledRender === null) { this.scheduledRender = setImmediate(() => this.render()) } } private changeIndex(by: number) { this.activeIndex += by if (this.activeIndex < 0) { this.activeIndex = 0 } else if (this.activeIndex > this.value.length) { this.activeIndex = this.value.length } this.readline.line = this.activeValue this.readline.cursor = this.activeValue.length } private onValidationError(isValid: false|string) { this.render(typeof isValid === "string" ? chalk.red(isValid) : chalk.dim.red("Invalid input")) } private onValidated(value: string[]) { this.value.splice(0, this.value.length, ...value) this.status = "answered" this.render() this.screen.done() this.done(value) } }