Skip to content

Commit

Permalink
feat(prompt): add search option to Select and Checkbox prompt
Browse files Browse the repository at this point in the history
  • Loading branch information
c4spar committed Dec 28, 2020
1 parent 7dd6660 commit 7d09739
Show file tree
Hide file tree
Showing 11 changed files with 336 additions and 145 deletions.
32 changes: 11 additions & 21 deletions prompt/_generic_input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,35 +19,30 @@ export interface GenericInputKeys {
}

/** Generic input prompt options. */
export interface GenericInputPromptOptions<T>
extends GenericPromptOptions<T, string> {
export interface GenericInputPromptOptions<T, V>
extends GenericPromptOptions<T, V> {
keys?: GenericInputKeys;
suggestions?: Array<string | number>;
}

/** Generic input prompt settings. */
export interface GenericInputPromptSettings<T>
extends GenericPromptSettings<T, string> {
export interface GenericInputPromptSettings<T, V>
extends GenericPromptSettings<T, V> {
keys?: GenericInputKeys;
suggestions?: Array<string | number>;
}

/** Generic input prompt representation. */
export abstract class GenericInput<T, S extends GenericInputPromptSettings<T>>
extends GenericPrompt<T, string, S> {
export abstract class GenericInput<
T,
V,
S extends GenericInputPromptSettings<T, V>,
> extends GenericPrompt<T, V, S> {
protected inputValue = "";
protected inputIndex = 0;
protected suggestionsIndex = 0;
protected suggestions: Array<string | number> = [];

/**
* Inject prompt value. Can be used for unit tests or pre selections.
* @param value Input value.
*/
public static inject(value: string): void {
GenericPrompt.inject(value);
}

/**
* Prompt constructor.
* @param settings Prompt settings.
Expand Down Expand Up @@ -109,11 +104,6 @@ export abstract class GenericInput<T, S extends GenericInputPromptSettings<T>>
);
}

/** Get user input. */
protected getValue(): string {
return this.inputValue;
}

/**
* Handle user input event.
* @param event Key event.
Expand Down Expand Up @@ -221,7 +211,7 @@ export abstract class GenericInput<T, S extends GenericInputPromptSettings<T>>

/** Select previous suggestion. */
protected selectPreviousSuggestion(): void {
if (this.settings.suggestions?.length) {
if (this.suggestions?.length) {
if (this.suggestionsIndex > 0) {
this.suggestionsIndex--;
}
Expand All @@ -230,7 +220,7 @@ export abstract class GenericInput<T, S extends GenericInputPromptSettings<T>>

/** Select next suggestion. */
protected selectNextSuggestion(): void {
if (this.settings.suggestions?.length) {
if (this.suggestions?.length) {
if (this.suggestionsIndex < this.suggestions.length - 1) {
this.suggestionsIndex++;
}
Expand Down
123 changes: 92 additions & 31 deletions prompt/_generic_list.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
import {
GenericPrompt,
GenericPromptOptions,
GenericPromptSettings,
} from "./_generic_prompt.ts";
GenericInput,
GenericInputPromptOptions,
GenericInputPromptSettings,
} from "./_generic_input.ts";
import { bold, dim, red, stripColor, yellow } from "./deps.ts";
import { Figures } from "./figures.ts";
import { InputKeys } from "./input.ts";
import { SelectOption } from "./select.ts";

type UnsupportedInputKeys =
| "complete"
| "selectNextHistory"
| "selectPreviousHistory";

/** Select key options. */
export interface GenericListKeys
extends Omit<InputKeys, UnsupportedInputKeys> {}

/** Generic list option options. */
export interface GenericListOption {
Expand All @@ -22,26 +35,31 @@ export type GenericListValueOptions = (string | GenericListOption)[];
export type GenericListValueSettings = GenericListOptionSettings[];

/** Generic list prompt options. */
export interface GenericListOptions<T, V> extends GenericPromptOptions<T, V> {
export interface GenericListOptions<T, V>
extends GenericInputPromptOptions<T, V> {
options: GenericListValueOptions;
indent?: string;
listPointer?: string;
maxRows?: number;
filter?: boolean | string;
}

/** Generic list prompt settings. */
export interface GenericListSettings<T, V> extends GenericPromptSettings<T, V> {
export interface GenericListSettings<T, V>
extends GenericInputPromptSettings<T, V> {
options: GenericListValueSettings;
indent: string;
listPointer: string;
maxRows: number;
filter?: boolean | string;
}

/** Generic list prompt representation. */
export abstract class GenericList<T, V, S extends GenericListSettings<T, V>>
extends GenericPrompt<T, V, S> {
protected index = 0;
protected selected = 0;
extends GenericInput<T, V, S> {
protected listOffset = 0;
protected listIndex = 0;
protected options: S["options"] = this.settings.options;

/**
* Create list separator.
Expand All @@ -65,18 +83,33 @@ export abstract class GenericList<T, V, S extends GenericListSettings<T, V>>
};
}

protected message(): string {
let message = ` ${yellow("?")} ` + bold(this.settings.message) +
this.defaults();
if (this.settings.filter) {
message += " " + this.settings.pointer + " ";
}
this.cursor.x = stripColor(message).length + this.inputIndex + 1;
return message + this.input();
}

/** Render options. */
protected body(): string | undefined | Promise<string | undefined> {
const body: Array<string> = [];
const height: number = this.getListHeight();
for (let i = this.index; i < this.index + height; i++) {
for (let i = this.listOffset; i < this.listOffset + height; i++) {
body.push(
this.getListItem(
this.settings.options[i],
this.selected === i,
this.options[i],
this.listIndex === i,
),
);
}
if (!body.length) {
body.push(
this.settings.indent + yellow(" No matches..."),
);
}
return body.join("\n");
}

Expand All @@ -90,44 +123,72 @@ export abstract class GenericList<T, V, S extends GenericListSettings<T, V>>
isSelected?: boolean,
): string;

protected match(needle: string = this.inputValue): void {
this.options = this.settings.options.filter(
(option: SelectOption) =>
(option.name ?? option.value).toString().toLowerCase().startsWith(
needle.toLowerCase(),
),
);
this.listIndex = Math.max(
0,
Math.min(this.options.length - 1, this.listIndex),
);
this.listOffset = Math.max(
0,
Math.min(
this.options.length - this.getListHeight(),
this.listOffset,
),
);
}

/** Read user input. */
protected read(): Promise<boolean> {
this.tty.cursorHide();
if (!this.settings.filter) {
this.tty.cursorHide();
}
return super.read();
}

/** Select previous option. */
protected selectPrevious(): void {
if (this.selected > 0) {
this.selected--;
if (this.selected < this.index) {
this.index--;
if (this.options.length < 2) {
return;
}
if (this.listIndex > 0) {
this.listIndex--;
if (this.listIndex < this.listOffset) {
this.listOffset--;
}
if (this.settings.options[this.selected].disabled) {
if (this.options[this.listIndex].disabled) {
this.selectPrevious();
}
} else {
this.selected = this.settings.options.length - 1;
this.index = this.settings.options.length - this.getListHeight();
if (this.settings.options[this.selected].disabled) {
this.listIndex = this.options.length - 1;
this.listOffset = this.options.length - this.getListHeight();
if (this.options[this.listIndex].disabled) {
this.selectPrevious();
}
}
}

/** Select next option. */
protected selectNext(): void {
if (this.selected < this.settings.options.length - 1) {
this.selected++;
if (this.selected >= this.index + this.getListHeight()) {
this.index++;
if (this.options.length < 2) {
return;
}
if (this.listIndex < this.options.length - 1) {
this.listIndex++;
if (this.listIndex >= this.listOffset + this.getListHeight()) {
this.listOffset++;
}
if (this.settings.options[this.selected].disabled) {
if (this.options[this.listIndex].disabled) {
this.selectNext();
}
} else {
this.selected = this.index = 0;
if (this.settings.options[this.selected].disabled) {
this.listIndex = this.listOffset = 0;
if (this.options[this.listIndex].disabled) {
this.selectNext();
}
}
Expand All @@ -136,8 +197,8 @@ export abstract class GenericList<T, V, S extends GenericListSettings<T, V>>
/** Get options row height. */
protected getListHeight(): number {
return Math.min(
this.settings.options.length,
this.settings.maxRows || this.settings.options.length,
this.options.length,
this.settings.maxRows || this.options.length,
);
}

Expand All @@ -148,6 +209,6 @@ export abstract class GenericList<T, V, S extends GenericListSettings<T, V>>
protected getOptionByValue(
value: string,
): GenericListOptionSettings | undefined {
return this.settings.options.find((option) => option.value === value);
return this.options.find((option) => option.value === value);
}
}
19 changes: 8 additions & 11 deletions prompt/_generic_prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,6 @@ export interface Cursor {
y: number;
}

export type RenderResult = [string, string | undefined, string | undefined];

/** Generic prompt representation. */
export abstract class GenericPrompt<
T,
Expand Down Expand Up @@ -149,17 +147,16 @@ export abstract class GenericPrompt<
if (typeof GenericPrompt.injectedValue !== "undefined") {
const value: V = GenericPrompt.injectedValue as V;
await this.#validateValue(value);
return true;
}

const events: KeyEvent[] = await this.#readKey();
} else {
const events: KeyEvent[] = await this.#readKey();

if (!events.length) {
return false;
}
if (!events.length) {
return false;
}

for (const event of events) {
await this.handleEvent(event);
for (const event of events) {
await this.handleEvent(event);
}
}

return typeof this.#value !== "undefined";
Expand Down

0 comments on commit 7d09739

Please sign in to comment.