You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
218 lines
7.9 KiB
218 lines
7.9 KiB
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<AnswerT extends Answers = Answers> extends Question<AnswerT>, MultiTextInputQuestionOptions {
|
|
/** @inheritDoc */
|
|
type: "multitext"
|
|
default?: string[]
|
|
}
|
|
|
|
export interface QuestionMap {
|
|
["multitext"]: HierarchicalCheckboxQuestion
|
|
}
|
|
}
|
|
|
|
export class MultiTextInput<AnswerT extends Answers = Answers, QuestionT extends MultiTextInputQuestion<AnswerT> = MultiTextInputQuestion> extends Prompt<QuestionT> {
|
|
|
|
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<string[]> = new Subject<string[]>()
|
|
// @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("<enter>")} to submit these entries, ${chalk.yellow("<ctrl>+x")} to delete all entries, or ${chalk.yellow("<up>")}/${chalk.yellow("<down>")} to change existing entries.`))
|
|
} else {
|
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} or ${chalk.yellow("<ctrl>+x")} to delete this entry or ${chalk.yellow("<ctrl>+r")} to revert.`))
|
|
}
|
|
} else {
|
|
outputs.push(this.readline.line)
|
|
if (this.isNew) {
|
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} to add this entry or ${chalk.yellow("<ctrl>+r")} or ${chalk.yellow("<ctrl>+x")} to clear it.`))
|
|
} else if (this.activeValue !== this.readline.line) {
|
|
bottomOutputs.push(chalk.dim(`Press ${chalk.yellow("<enter>")} to save this entry, ${chalk.yellow("<ctrl>+r")} to revert it, or ${chalk.yellow("<ctrl>+x")} to delete it.`))
|
|
} else {
|
|
bottomOutputs.push(chalk.dim(`Begin typing to change this entry or press ${chalk.yellow("<ctrl>+x")} to delete it. Press ${chalk.yellow("<up>")}/${chalk.yellow("<down>")} 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)
|
|
}
|
|
} |