Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement filter list #124

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions examples/test/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as p from '@clack/prompts';

async function main() {
console.clear();
p.select({
options: [{ value: 'basic', label: 'Basic' }],
message: 'Select an example to run.',
enableFilter: true,
});
}

main().catch(console.error);
17 changes: 17 additions & 0 deletions examples/test/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"name": "@example/test",
"private": true,
"version": "0.0.0",
"type": "module",
"dependencies": {
"@clack/core": "workspace:*",
"@clack/prompts": "workspace:*",
"picocolors": "^1.0.0"
},
"scripts": {
"start": "jiti ./index.ts"
},
"devDependencies": {
"jiti": "^1.17.0"
}
}
3 changes: 3 additions & 0 deletions examples/test/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"sisteransi": "^1.0.5"
},
"devDependencies": {
"wrap-ansi": "^8.1.0"
"wrap-ansi": "^8.1.0",
"fzf": "^0.5.2"
}
}
32 changes: 30 additions & 2 deletions packages/core/src/prompts/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { Readable, Writable } from 'node:stream';
import { WriteStream } from 'node:tty';
import { cursor, erase } from 'sisteransi';
import wrap from 'wrap-ansi';
import { Fzf } from 'fzf';

function diffLines(a: string, b: string) {
if (a === b) return;
Expand Down Expand Up @@ -46,6 +47,7 @@ export interface PromptOptions<Self extends Prompt> {
input?: Readable;
output?: Writable;
debug?: boolean;
enableFilter?: boolean;
}

export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error';
Expand All @@ -58,6 +60,7 @@ export default class Prompt {
private _track: boolean = false;
private _render: (context: Omit<Prompt, 'prompt'>) => string | void;
protected _cursor: number = 0;
private _filterKey = '';

public state: State = 'initial';
public value: any;
Expand Down Expand Up @@ -136,7 +139,7 @@ export default class Prompt {
arr.push({ cb, once: true });
this.subscribers.set(event, arr);
}
public emit(event: string, ...data: any[]) {
public emit<T extends any>(event: string, ...data: T[]) {
const cbs = this.subscribers.get(event) ?? [];
const cleanup: (() => void)[] = [];
for (const subscriber of cbs) {
Expand All @@ -157,7 +160,12 @@ export default class Prompt {
if (this.state === 'error') {
this.state = 'active';
}
if (key?.name && !this._track && aliases.has(key.name)) {
if (
key?.name &&
!this._track &&
/* disable moving aliases when enable filter */
(this.opts.enableFilter ? false : aliases.has(key.name))
) {
this.emit('cursor', aliases.get(key.name));
}
if (key?.name && keys.has(key.name)) {
Expand Down Expand Up @@ -252,4 +260,24 @@ export default class Prompt {
}
this._prevFrame = frame;
}

protected registerFilterer(list: string[]) {
if (this.opts.enableFilter) {
const fzf = new Fzf(list);
this.on('key', (key) => {
if (key === /* backspace */ '\x7F') {
if (this._filterKey.length) this._filterKey = this._filterKey.slice(0, -1);
if (!this._filterKey.length) this.emit('filterClear');
} else if (key.length === 1 && key !== ' ') {
this._filterKey += key;
}
const filtered = fzf.find(this._filterKey);
this.emit('filter', filtered);
});
}
}

getFilterKey() {
return this._filterKey;
}
}
22 changes: 21 additions & 1 deletion packages/core/src/prompts/select.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import Prompt, { PromptOptions } from './prompt';
import { type FzfResultItem } from 'fzf';

interface SelectOptions<T extends { value: any }> extends PromptOptions<SelectPrompt<T>> {
options: T[];
initialValue?: T['value'];
enableFilter?: boolean;
}
export default class SelectPrompt<T extends { value: any }> extends Prompt {
originalOptions: T[] = [];
options: T[];
cursor: number = 0;

Expand All @@ -19,7 +22,7 @@ export default class SelectPrompt<T extends { value: any }> extends Prompt {
constructor(opts: SelectOptions<T>) {
super(opts, false);

this.options = opts.options;
this.originalOptions = this.options = opts.options;
this.cursor = this.options.findIndex(({ value }) => value === opts.initialValue);
if (this.cursor === -1) this.cursor = 0;
this.changeValue();
Expand All @@ -37,5 +40,22 @@ export default class SelectPrompt<T extends { value: any }> extends Prompt {
}
this.changeValue();
});

// For filter
this.registerFilterer(this.options.map(({ value }) => value));
this.on('filter', (filtered: FzfResultItem[]) => {
this.cursor = 0;
if (filtered.length) {
this.options = filtered.map(
({ item }) => this.originalOptions.find(({ value }) => value === item)!
);
} else {
this.options = [];
}
Comment on lines +48 to +54
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe a shorter version:

Suggested change
if (filtered.length) {
this.options = filtered.map(
({ item }) => this.originalOptions.find(({ value }) => value === item)!
);
} else {
this.options = [];
}
this.options = filtered.map(
({ item }) => this.originalOptions.find(({ value }) => value === item)!
);

});
this.on('filterClear', () => {
this.cursor = 0;
this.options = this.originalOptions;
});
}
}
9 changes: 8 additions & 1 deletion packages/prompts/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ export interface SelectOptions<Options extends Option<Value>[], Value> {
message: string;
options: Options;
initialValue?: Value;
enableFilter?: boolean;
}

export const select = <Options extends Option<Value>[], Value>(
Expand All @@ -200,9 +201,15 @@ export const select = <Options extends Option<Value>[], Value>(
return new SelectPrompt({
options: opts.options,
initialValue: opts.initialValue,
enableFilter: opts.enableFilter,
render() {
const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`;

const filterKey = this.getFilterKey().length
? this.getFilterKey()
: color.gray('Type to filter...');
const filterer = opts.enableFilter ? ` > ${filterKey}\n${color.cyan(S_BAR)} ` : '';

switch (this.state) {
case 'submit':
return `${title}${color.gray(S_BAR)} ${opt(this.options[this.cursor], 'selected')}`;
Expand All @@ -212,7 +219,7 @@ export const select = <Options extends Option<Value>[], Value>(
'cancelled'
)}\n${color.gray(S_BAR)}`;
default: {
return `${title}${color.cyan(S_BAR)} ${this.options
return `${title}${color.cyan(S_BAR)}${filterer}${this.options
.map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive'))
.join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`;
}
Expand Down
19 changes: 19 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.