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.
672 lines
26 KiB
672 lines
26 KiB
import Prompt from "inquirer/lib/prompts/base";
|
|
import observe from "inquirer/lib/utils/events";
|
|
import {
|
|
Answers,
|
|
ChoiceOptions,
|
|
HierarchicalCheckboxChoice,
|
|
HierarchicalCheckboxQuestion,
|
|
Question,
|
|
Separator,
|
|
SeparatorOptions,
|
|
} 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";
|
|
import {isPopulatedArray} from "../../utils/Arrays";
|
|
import {filter as fuzzyFilter, FilterOptions} from "fuzzy";
|
|
|
|
interface ExtendedReadLine extends ReadLineInterface {
|
|
line: string
|
|
cursor: number
|
|
}
|
|
|
|
declare module "inquirer" {
|
|
|
|
export interface HierarchicalCheckboxChoiceBase extends ChoiceOptions {
|
|
readonly value?: string|null
|
|
readonly children?: readonly HierarchicalCheckboxChoice[]
|
|
readonly showChildren?: boolean
|
|
}
|
|
export interface HierarchicalCheckboxParentChoice extends HierarchicalCheckboxChoiceBase {
|
|
readonly showChildren?: boolean
|
|
readonly value?: null
|
|
readonly children: readonly [HierarchicalCheckboxChoice, ...HierarchicalCheckboxChoice[]]
|
|
}
|
|
export interface HierarchicalCheckboxChildChoice extends HierarchicalCheckboxChoiceBase {
|
|
readonly value: string|null
|
|
readonly children?: readonly []
|
|
readonly showChildren?: false
|
|
}
|
|
|
|
export type HierarchicalCheckboxChoice = HierarchicalCheckboxParentChoice|HierarchicalCheckboxChildChoice
|
|
|
|
export interface HierarchicalCheckboxQuestionOptions {
|
|
readonly choices: readonly (HierarchicalCheckboxChoice|SeparatorOptions)[]
|
|
readonly pageSize?: number
|
|
}
|
|
|
|
export interface HierarchicalCheckboxQuestion<AnswerT extends Answers = Answers> extends Question<AnswerT>, HierarchicalCheckboxQuestionOptions {
|
|
/** @inheritDoc */
|
|
type: "hierarchical-checkbox"
|
|
default?: string[]
|
|
}
|
|
|
|
export interface QuestionMap {
|
|
["hierarchical-checkbox"]: HierarchicalCheckboxQuestion
|
|
}
|
|
}
|
|
|
|
interface NormalizedChoiceBase {
|
|
readonly name: string
|
|
readonly value: string|null
|
|
readonly short: string|null
|
|
readonly children?: readonly NormalizedChoice[]
|
|
}
|
|
|
|
interface NormalizedParentChoice extends NormalizedChoiceBase {
|
|
readonly name: string
|
|
readonly value: null
|
|
readonly short: null
|
|
readonly children: readonly [NormalizedChoice, ...NormalizedChoice[]]
|
|
readonly showChildren: boolean
|
|
}
|
|
|
|
interface NormalizedLeafChoice extends NormalizedChoiceBase {
|
|
readonly name: string
|
|
readonly value: string
|
|
readonly short: string
|
|
readonly children?: readonly []
|
|
}
|
|
|
|
interface NormalizedDisabledChoice extends NormalizedChoiceBase {
|
|
readonly name: string
|
|
readonly value: null
|
|
readonly short: null
|
|
readonly children?: readonly []
|
|
}
|
|
type NormalizedChoice = NormalizedParentChoice|NormalizedLeafChoice|NormalizedDisabledChoice
|
|
|
|
function isParentChoice(c: NormalizedChoice): c is NormalizedParentChoice {
|
|
return isPopulatedArray(c.children)
|
|
}
|
|
function isLeafChoice(c: NormalizedChoice): c is NormalizedLeafChoice {
|
|
return c.value !== null
|
|
}
|
|
function isDisabledChoice(c: NormalizedChoice): c is NormalizedDisabledChoice {
|
|
return c.value === null && !isPopulatedArray(c.children)
|
|
}
|
|
|
|
function normalizeChoice(choice: HierarchicalCheckboxChoice|SeparatorOptions, valueMap: ValueMap): NormalizedChoice {
|
|
if (choice.type === "separator") {
|
|
return {
|
|
name: choice.line || new Separator().line,
|
|
value: null,
|
|
short: null,
|
|
children: [],
|
|
}
|
|
}
|
|
const { name = chalk.dim("undefined") } = choice
|
|
const children = isPopulatedArray(choice.children) ? choice.children.map((choice) => normalizeChoice(choice, valueMap)) : [];
|
|
if (isPopulatedArray(children)) {
|
|
const { showChildren = true } = choice
|
|
return {
|
|
name,
|
|
value: null,
|
|
short: null,
|
|
children,
|
|
showChildren,
|
|
}
|
|
} else {
|
|
const { name: originalName = null } = choice
|
|
const { value = originalName } = choice
|
|
if (value === null) {
|
|
// Disabled choice, no need for a short value
|
|
return {
|
|
name,
|
|
value: null,
|
|
short: null,
|
|
children: [],
|
|
}
|
|
} else {
|
|
// Selectable leaf choice
|
|
const { short = name } = choice
|
|
const result: NormalizedLeafChoice = {
|
|
name,
|
|
value,
|
|
short,
|
|
children: []
|
|
}
|
|
addValueToMap(result, valueMap)
|
|
return result
|
|
}
|
|
}
|
|
}
|
|
|
|
interface BaseBreadcrumb {
|
|
readonly parent: Breadcrumb|null
|
|
readonly index: number|null
|
|
readonly children: readonly NormalizedChoice[]
|
|
readonly searchable: readonly (NormalizedLeafChoice|NormalizedParentChoice)[]
|
|
}
|
|
|
|
interface RootBreadcrumb extends BaseBreadcrumb {
|
|
readonly parent: null
|
|
readonly index: null
|
|
readonly selectable: readonly number[]
|
|
}
|
|
|
|
interface ChildBreadcrumb extends BaseBreadcrumb {
|
|
readonly parent: Breadcrumb
|
|
readonly index: number
|
|
readonly selectable: readonly number[]
|
|
}
|
|
|
|
interface FilterBreadcrumb extends BaseBreadcrumb {
|
|
readonly parent: Breadcrumb
|
|
readonly index: null
|
|
readonly filter: string
|
|
readonly adjustingFilter: boolean
|
|
}
|
|
|
|
type Breadcrumb = RootBreadcrumb|ChildBreadcrumb|FilterBreadcrumb
|
|
|
|
function isRootBreadcrumb(b: Breadcrumb): b is RootBreadcrumb {
|
|
return b.parent === null && b.index === null
|
|
}
|
|
function isFilterBreadcrumb(b: Breadcrumb): b is FilterBreadcrumb {
|
|
return b.parent !== null && b.index === null
|
|
}
|
|
|
|
interface ValueMap {
|
|
[value: string]: NormalizedLeafChoice|[NormalizedLeafChoice, NormalizedLeafChoice, ...NormalizedLeafChoice[]]
|
|
}
|
|
interface ValidValueMap extends ValueMap {
|
|
[value: string]: NormalizedLeafChoice
|
|
}
|
|
interface InvalidValueMap extends ValueMap {
|
|
[value: string]: [NormalizedLeafChoice, NormalizedLeafChoice, ...NormalizedLeafChoice[]]
|
|
}
|
|
|
|
function addValueToMap(choice: NormalizedLeafChoice, map: ValueMap) {
|
|
if (map.hasOwnProperty(choice.value)) {
|
|
const existing = map[choice.value]
|
|
if (Array.isArray(existing) && !existing.some((item) => item.name === choice.name && item.short === choice.short)) {
|
|
map[choice.value] = [choice, ...existing]
|
|
} else if (!Array.isArray(existing) && !(existing.name === choice.name && existing.short === choice.short)) {
|
|
map[choice.value] = [choice, existing]
|
|
} else {
|
|
// It's a duplicate, but one that's the same as something already in the books.
|
|
}
|
|
} else {
|
|
map[choice.value] = choice
|
|
}
|
|
}
|
|
|
|
function splitValues(map: ValueMap): {valid: ValidValueMap, invalid: InvalidValueMap} {
|
|
const valid: ValidValueMap = {}
|
|
const invalid: InvalidValueMap = {}
|
|
Object.keys(map).forEach((value) => {
|
|
const item = map[value]
|
|
if (Array.isArray(item)) {
|
|
invalid[value] = item
|
|
} else {
|
|
valid[value] = item
|
|
}
|
|
})
|
|
return { valid, invalid }
|
|
}
|
|
|
|
function getSearchableList(base: readonly NormalizedChoice[]): (NormalizedLeafChoice|NormalizedParentChoice)[] {
|
|
const result = base.flatMap((choice) => {
|
|
if (isParentChoice(choice)) {
|
|
return [choice, ...getSearchableList(choice.children)]
|
|
} else if (isLeafChoice(choice)) {
|
|
return [choice]
|
|
} else {
|
|
return []
|
|
}
|
|
})
|
|
result.sort((a: NormalizedLeafChoice|NormalizedParentChoice, b: NormalizedLeafChoice|NormalizedParentChoice): number => a.name.localeCompare(b.name))
|
|
return result.filter((value, index) => result.findIndex((match) => match.name === value.name && match.value === value.value) === index)
|
|
}
|
|
|
|
function getSelectableList(base: readonly NormalizedChoice[]): number[] {
|
|
return base.map((_, index) => index).filter((choice) => !isDisabledChoice(base[choice]))
|
|
}
|
|
|
|
const [searchHighlightPrefix, searchHighlightSuffix] = chalk.yellowBright.bold("Placeholder").split("Placeholder") as [string, string]
|
|
|
|
export class HierarchicalCheckboxInput<AnswerT extends Answers = Answers, QuestionT extends HierarchicalCheckboxQuestion<AnswerT> = HierarchicalCheckboxQuestion> extends Prompt<QuestionT> {
|
|
|
|
constructor(question: QuestionT, readline: ExtendedReadLine, answers: AnswerT) {
|
|
super(question, readline, answers);
|
|
const valueMap: ValueMap = {}
|
|
const rootChoices = question.choices.map((choice) => normalizeChoice(choice, valueMap))
|
|
this.location = {
|
|
parent: null,
|
|
index: null,
|
|
children: rootChoices,
|
|
selectable: getSelectableList(rootChoices),
|
|
searchable: getSearchableList(rootChoices)
|
|
}
|
|
const { valid, invalid } = splitValues(valueMap)
|
|
this.valueMap = valid
|
|
this.invalidValues = invalid
|
|
this.value = question.default?.slice() || []
|
|
this.activeIndex = 0
|
|
this.updateUnlistedValues()
|
|
}
|
|
|
|
// @ts-ignore
|
|
_run(callback: HierarchicalCheckboxInput["done"], error: (err: Error) => void) {
|
|
const invalidChoices = Object.keys(this.invalidValues);
|
|
const invalidValues = this.value.filter((value) => !this.valueMap.hasOwnProperty(value));
|
|
if (isPopulatedArray(invalidChoices)) {
|
|
error(Error(`Duplicate values: ${invalidChoices.join()}`))
|
|
} else if (isPopulatedArray(invalidValues)) {
|
|
error(Error(`Unknown values: ${invalidValues.join()}`))
|
|
}
|
|
this.rl.on("history", (history) => {
|
|
history.splice(0, history.length)
|
|
})
|
|
const events = observe(this.rl);
|
|
this.status = "touched"
|
|
this.done = callback
|
|
events.keypress.pipe(filter((event) => {
|
|
return event.key.name !== "up" && event.key.name !== "down" && event.key.name !== "tab" && event.key.name !== "backspace" && event.key.name !== "space" && !(event.key.name === "x" && event.key.ctrl === true)
|
|
})).subscribe(() => this.onOtherKey())
|
|
events.normalizedUpKey.subscribe(() => this.onUpKey())
|
|
events.normalizedDownKey.subscribe(() => this.onDownKey())
|
|
events.keypress.pipe(filter((event) => event.key.name === "tab")).subscribe(() => this.onTab())
|
|
events.keypress.pipe(filter((event) => event.key.name === "backspace")).subscribe(() => this.onBack())
|
|
events.keypress.pipe(filter((event) => event.key.name === "space")).subscribe(() => this.onSpacebar())
|
|
events.line.subscribe(() => this.onLine())
|
|
events.keypress.pipe(filter((event) => event.key.name === "x" && event.key.ctrl === true)).subscribe(() => this.onClear())
|
|
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 readonly valueMap: ValidValueMap
|
|
private readonly invalidValues: InvalidValueMap
|
|
private readonly value: string[]
|
|
private done: (state: any) => void|null
|
|
private location: Breadcrumb
|
|
private activeIndex: number
|
|
private unlistedValues: string[]
|
|
private submitter: Subject<string[]> = new Subject<string[]>()
|
|
// @ts-ignore
|
|
private paginator: Paginator = new Paginator(this.screen, {isInfinite: true});
|
|
private scheduledRender: NodeJS.Immediate|null = null
|
|
|
|
private get readline(): ExtendedReadLine {
|
|
return this.rl
|
|
}
|
|
|
|
private get activeChoice(): NormalizedChoice|null {
|
|
if (this.activeIndex >= this.totalLength) {
|
|
return null
|
|
}
|
|
if (this.isUnlistedActive()) {
|
|
return this.valueMap[this.unlistedValues[this.activeUnlistedIndex()]] || null
|
|
} else {
|
|
return this.location.children[this.activeChildIndex()] || null
|
|
}
|
|
}
|
|
|
|
private render(error?: string) {
|
|
this.scheduledRender = null
|
|
const outputs: string[] = [this.getQuestion()]
|
|
const bottomOutputs: string[] = []
|
|
const isRoot = isRootBreadcrumb(this.location)
|
|
const isFilter = isFilterBreadcrumb(this.location)
|
|
const hasFilter = isFilterBreadcrumb(this.location) && this.location.filter !== ""
|
|
const isAdjustingFilter = isFilterBreadcrumb(this.location) && this.location.adjustingFilter
|
|
const isParent = this.activeChoice && isParentChoice(this.activeChoice)
|
|
const isLeaf = this.activeChoice && isLeafChoice(this.activeChoice)
|
|
if (this.status === "answered") {
|
|
outputs.push(this.value.map((s) => `${chalk.cyan(this.valueMap[s].short)}`).join(", "))
|
|
} else {
|
|
if (typeof error === "string") {
|
|
bottomOutputs.push(`${figures.warning} ${error}`)
|
|
} else {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
outputs.push(isAdjustingFilter ? chalk.cyan("Searching: ") : chalk.dim("Searching: "))
|
|
outputs.push(this.location.filter)
|
|
}
|
|
bottomOutputs.push(this.value.map((s) => this.valueMap[s].short).join(", ") || chalk.dim("(nothing selected)"))
|
|
if (isAdjustingFilter) {
|
|
bottomOutputs.push(chalk.dim(`Type to search, ${hasFilter ? "" : `${chalk.yellow("<backspace>")} to stop searching, `}`
|
|
+ `${chalk.yellow("<tab>")} to switch to the search results, `
|
|
+ `${chalk.yellow("<Ctrl>+x")} to stop searching, ${chalk.yellow("<enter>")} to select the current option and finish the search.`))
|
|
|
|
} else {
|
|
bottomOutputs.push(chalk.dim(`Use arrow keys to move, ${isRoot ? "" : isFilter ? hasFilter ? "" : `${chalk.yellow("<backspace>")} to stop searching, ` : `${chalk.yellow("<backspace>")} to ascend, `}`
|
|
+ (isFilter ? `${chalk.yellow("<tab>")} to switch to the search box, ` : `${chalk.yellow("<tab>")} to search this view, `)
|
|
+ `${isParent ? `${chalk.yellow("<space>")} to descend, ` : isLeaf ? `${chalk.yellow("<space>")} to toggle, ` : ""}`
|
|
+ `${isFilter ? `${chalk.yellow("<Ctrl>+x")} to stop searching, ` : isPopulatedArray(this.value) ? `${chalk.yellow("<Ctrl>+x")} to clear selection, ` : ""}${isFilter ? `${chalk.yellow("<enter>")} to select the current option and finish the search.` : `${chalk.yellow("<enter>")} to submit.`}`))
|
|
}
|
|
}
|
|
bottomOutputs.push(this.renderList())
|
|
}
|
|
this.screen.render(outputs.join(""), bottomOutputs.join("\n"))
|
|
}
|
|
|
|
private renderList(): string {
|
|
const outputs: string[] = []
|
|
const isAdjustingFilter = isFilterBreadcrumb(this.location) && this.location.adjustingFilter
|
|
|
|
outputs.push(...this.location.children.map((entry, index):string => {
|
|
const isSelected = index === this.activeChildIndex()
|
|
const isActive = isLeafChoice(entry) && this.value.includes(entry.value)
|
|
const isDisabled = isDisabledChoice(entry)
|
|
const isParent = isParentChoice(entry)
|
|
const showChildren = isParentChoice(entry) && entry.showChildren
|
|
return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isDisabled ? " " : isParent ? figures.arrowRight : isActive ? chalk.greenBright(figures.checkboxCircleOn) : figures.checkboxCircleOff} ${isSelected && !isAdjustingFilter ? (isActive ? chalk.blueBright(entry.name) : chalk.cyan(entry.name)) : (isActive ? chalk.green(entry.name) : entry.name)}${isParentChoice(entry) && showChildren ? `${chalk.dim(` (${entry.children.map((child) => child.name).join(", ")})`)}` : ""}`
|
|
}))
|
|
|
|
if (!isFilterBreadcrumb(this.location) && isPopulatedArray(this.unlistedValues)) {
|
|
outputs.push(" " + new Separator().line)
|
|
outputs.push(...this.unlistedValues.map((value, index): string => {
|
|
const isSelected = index === this.activeUnlistedIndex()
|
|
const isActive = this.value.includes(value)
|
|
const entry = this.valueMap[value]
|
|
return `${isSelected ? chalk.cyan(figures.pointer) : " "} ${isActive ? chalk.greenBright(figures.checkboxCircleOn) : figures.checkboxCircleOff} ${isSelected ? (isActive ? chalk.blueBright(entry.name) : chalk.cyan(entry.name)) : (isActive ? chalk.green(entry.name) : entry.name)}`
|
|
}))
|
|
}
|
|
|
|
outputs.push(" " + new Separator().line)
|
|
|
|
// @ts-ignore
|
|
return this.paginator.paginate(outputs.join("\n"), this.activeIndex, this.opt.pageSize)
|
|
}
|
|
|
|
private isUnlistedActive() {
|
|
return !isFilterBreadcrumb(this.location) && this.activeUnlistedIndex() >= 0
|
|
}
|
|
|
|
private activeUnlistedIndex() {
|
|
if (isFilterBreadcrumb(this.location) || (this.activeIndex < this.location.selectable.length)) {
|
|
return -1;
|
|
} else {
|
|
return this.activeIndex - this.location.selectable.length;
|
|
}
|
|
}
|
|
|
|
private activeChildIndex() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
return this.activeIndex
|
|
} else {
|
|
if (this.isUnlistedActive()) {
|
|
return -1
|
|
} else {
|
|
return this.location.selectable[this.activeIndex];
|
|
}
|
|
}
|
|
}
|
|
|
|
private onUpKey() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
if (this.location.adjustingFilter) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: false
|
|
}
|
|
}
|
|
}
|
|
if (this.activeIndex > 0) {
|
|
this.changeIndex(-1)
|
|
} else {
|
|
this.activeIndex = this.totalLength
|
|
this.changeIndex(-1)
|
|
}
|
|
this.scheduleRender()
|
|
}
|
|
|
|
private onDownKey() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
if (this.location.adjustingFilter) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: false
|
|
}
|
|
}
|
|
}
|
|
if (this.activeIndex < this.totalLength - 1) {
|
|
this.changeIndex(+1)
|
|
} else {
|
|
this.activeIndex = -1
|
|
this.changeIndex(+1)
|
|
}
|
|
this.scheduleRender()
|
|
}
|
|
|
|
private onTab() {
|
|
if (!isFilterBreadcrumb(this.location)) {
|
|
this.readline.line = ""
|
|
this.readline.cursor = 0
|
|
this.updateFilter()
|
|
} else if (this.location.adjustingFilter) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: false
|
|
}
|
|
this.readline.line = this.location.filter
|
|
this.readline.cursor = this.location.filter.length
|
|
} else {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: true
|
|
}
|
|
this.readline.line = this.location.filter
|
|
this.readline.cursor = this.location.filter.length
|
|
}
|
|
|
|
this.scheduleRender()
|
|
}
|
|
|
|
private onBack() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
if (!this.location.adjustingFilter && (this.location.filter !== this.readline.line || this.readline.cursor !== this.location.filter.length)) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: true
|
|
}
|
|
}
|
|
if (this.location.filter !== "") {
|
|
this.updateFilter()
|
|
this.scheduleRender()
|
|
return
|
|
}
|
|
}
|
|
const oldLocation = this.location
|
|
if (isRootBreadcrumb(oldLocation)) {
|
|
return
|
|
}
|
|
if (isFilterBreadcrumb(oldLocation)) {
|
|
this.location = oldLocation.parent
|
|
this.activeIndex = 0
|
|
} else if (isFilterBreadcrumb(oldLocation.parent)) {
|
|
this.location = oldLocation.parent.parent
|
|
this.activeIndex = 0
|
|
} else {
|
|
this.location = oldLocation.parent
|
|
this.activeIndex = oldLocation.index
|
|
}
|
|
this.updateUnlistedValues()
|
|
this.updatePrompt()
|
|
this.scheduleRender()
|
|
}
|
|
|
|
private onSpacebar() {
|
|
if (isFilterBreadcrumb(this.location) && this.location.adjustingFilter) {
|
|
this.updateFilter()
|
|
this.scheduleRender()
|
|
return
|
|
}
|
|
const currentChoice = this.activeChoice
|
|
if (currentChoice === null) {
|
|
return
|
|
} else if (isLeafChoice(currentChoice)) {
|
|
const index = this.value.indexOf(currentChoice.value)
|
|
if (index === -1) {
|
|
this.value.push(currentChoice.value)
|
|
} else {
|
|
this.value.splice(index, 1)
|
|
}
|
|
this.scheduleRender()
|
|
} else if (isParentChoice(currentChoice)) {
|
|
this.location = {
|
|
parent: this.location,
|
|
index: this.activeIndex,
|
|
children: currentChoice.children,
|
|
selectable: getSelectableList(currentChoice.children),
|
|
searchable: getSearchableList(currentChoice.children),
|
|
}
|
|
this.activeIndex = 0
|
|
this.updateUnlistedValues()
|
|
this.scheduleRender()
|
|
} else {
|
|
// disabled choice, do nothing
|
|
}
|
|
}
|
|
|
|
private updateUnlistedValues() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
this.unlistedValues = []
|
|
} else {
|
|
this.unlistedValues = this.value.filter((value) => !this.location.children.some((choice) => choice.value === value))
|
|
}
|
|
}
|
|
|
|
private updateFilter() {
|
|
if (!isFilterBreadcrumb(this.location)) {
|
|
this.location = {
|
|
parent: this.location,
|
|
index: null,
|
|
filter: "",
|
|
adjustingFilter: true,
|
|
children: this.location.searchable,
|
|
searchable: this.location.searchable,
|
|
}
|
|
this.activeIndex = 0
|
|
this.updatePrompt()
|
|
}
|
|
if (this.location.adjustingFilter && this.readline.line !== this.location.filter) {
|
|
this.location = {
|
|
...this.location,
|
|
filter: this.readline.line,
|
|
children: this.readline.line === "" ? this.location.searchable : fuzzyFilter(this.readline.line, this.location.searchable.slice(), {
|
|
pre: searchHighlightPrefix,
|
|
post: searchHighlightSuffix,
|
|
extract(input: any): string {
|
|
return input.name
|
|
}
|
|
} as FilterOptions<NormalizedParentChoice | NormalizedLeafChoice>).map((result) => ({
|
|
...result.original,
|
|
name: result.string,
|
|
}))
|
|
}
|
|
this.activeIndex = 0
|
|
}
|
|
}
|
|
|
|
private onLine() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
if (!isPopulatedArray(this.location.children)) {
|
|
return
|
|
}
|
|
if (this.location.adjustingFilter) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: false,
|
|
}
|
|
}
|
|
// Activate the selected item.
|
|
this.onSpacebar()
|
|
const newLocation = this.location
|
|
if (isFilterBreadcrumb(newLocation)) {
|
|
this.location = this.location.parent
|
|
}
|
|
this.updateUnlistedValues()
|
|
this.updatePrompt()
|
|
this.scheduleRender()
|
|
} else {
|
|
this.submitter.next(this.value.slice())
|
|
}
|
|
}
|
|
|
|
private onClear() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
this.location = this.location.parent
|
|
this.activeIndex = 0
|
|
this.updateUnlistedValues()
|
|
this.updatePrompt()
|
|
this.scheduleRender()
|
|
}
|
|
this.value.splice(0, this.value.length)
|
|
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.totalLength) {
|
|
this.activeIndex = this.totalLength - 1
|
|
}
|
|
}
|
|
|
|
private get totalLength() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
return this.location.children.length;
|
|
} else {
|
|
return this.location.selectable.length + this.unlistedValues.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)
|
|
}
|
|
|
|
private onOtherKey() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
if (!this.location.adjustingFilter && (this.location.filter !== this.readline.line || this.readline.cursor !== this.location.filter.length)) {
|
|
this.location = {
|
|
...this.location,
|
|
adjustingFilter: true
|
|
}
|
|
}
|
|
this.updateFilter()
|
|
} else if (this.readline.line !== "") {
|
|
this.updateFilter()
|
|
}
|
|
this.scheduleRender()
|
|
}
|
|
|
|
private updatePrompt() {
|
|
if (isFilterBreadcrumb(this.location)) {
|
|
this.readline.setPrompt("Searching: ")
|
|
} else {
|
|
this.readline.setPrompt("")
|
|
}
|
|
}
|
|
} |