Mari's guided journal software.
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.
 
 
mari-guided-journal/src/prompts/types/MultiTextInput.ts

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)
}
}