From bce9ec900129abc9f697fd50462b426a0a7b5f23 Mon Sep 17 00:00:00 2001 From: alexzhang1030 <1642114555@qq.com> Date: Thu, 25 May 2023 17:57:52 +0800 Subject: [PATCH 1/4] chore: initial setup --- examples/test/index.ts | 12 +++ examples/test/package.json | 17 ++++ examples/test/tsconfig.json | 3 + packages/core/package.json | 3 +- packages/core/src/prompts/prompt.ts | 32 ++++++- packages/core/src/prompts/select.ts | 22 ++++- packages/prompts/package.json | 130 ++++++++++++++-------------- packages/prompts/src/index.ts | 4 +- pnpm-lock.yaml | 19 ++++ 9 files changed, 172 insertions(+), 70 deletions(-) create mode 100644 examples/test/index.ts create mode 100644 examples/test/package.json create mode 100644 examples/test/tsconfig.json diff --git a/examples/test/index.ts b/examples/test/index.ts new file mode 100644 index 00000000..a51f74ef --- /dev/null +++ b/examples/test/index.ts @@ -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); diff --git a/examples/test/package.json b/examples/test/package.json new file mode 100644 index 00000000..5f13c9a4 --- /dev/null +++ b/examples/test/package.json @@ -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" + } +} \ No newline at end of file diff --git a/examples/test/tsconfig.json b/examples/test/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/examples/test/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/packages/core/package.json b/packages/core/package.json index b928edbc..0ebe0d06 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -57,6 +57,7 @@ "sisteransi": "^1.0.5" }, "devDependencies": { - "wrap-ansi": "^8.1.0" + "wrap-ansi": "^8.1.0", + "fzf": "^0.5.2" } } diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 21ee0077..88685426 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -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; @@ -46,6 +47,7 @@ export interface PromptOptions { input?: Readable; output?: Writable; debug?: boolean; + enableFilter?: boolean; } export type State = 'initial' | 'active' | 'cancel' | 'submit' | 'error'; @@ -58,6 +60,7 @@ export default class Prompt { private _track: boolean = false; private _render: (context: Omit) => string | void; protected _cursor: number = 0; + private _filterKey = ''; public state: State = 'initial'; public value: any; @@ -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(event: string, ...data: T[]) { const cbs = this.subscribers.get(event) ?? []; const cleanup: (() => void)[] = []; for (const subscriber of cbs) { @@ -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)) { @@ -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('filtered', filtered); + }); + } + } + + getFilterKey() { + return this._filterKey; + } } diff --git a/packages/core/src/prompts/select.ts b/packages/core/src/prompts/select.ts index 521764e2..fcee3b6a 100644 --- a/packages/core/src/prompts/select.ts +++ b/packages/core/src/prompts/select.ts @@ -1,10 +1,13 @@ import Prompt, { PromptOptions } from './prompt'; +import { Fzf, FzfResultItem } from 'fzf'; interface SelectOptions extends PromptOptions> { options: T[]; initialValue?: T['value']; + enableFilter?: boolean; } export default class SelectPrompt extends Prompt { + originalOptions: T[] = []; options: T[]; cursor: number = 0; @@ -19,7 +22,7 @@ export default class SelectPrompt extends Prompt { constructor(opts: SelectOptions) { 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(); @@ -37,5 +40,22 @@ export default class SelectPrompt extends Prompt { } this.changeValue(); }); + + // For filter + this.registerFilterer(this.options.map(({ value }) => value)); + this.on('filtered', (filtered: FzfResultItem[]) => { + this.cursor = 0; + if (filtered.length) { + this.options = filtered.map( + ({ item }) => this.originalOptions.find(({ value }) => value === item)! + ); + } else { + this.options = []; + } + }); + this.on('filterClear', () => { + this.cursor = 0; + this.options = this.originalOptions; + }); } } diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 52326461..1a9f0361 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -1,66 +1,66 @@ { - "name": "@clack/prompts", - "version": "0.6.3", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" - }, - "./package.json": "./package.json" - }, - "types": "./dist/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/natemoo-re/clack", - "directory": "packages/prompts" - }, - "bugs": { - "url": "https://github.com/natemoo-re/clack/issues" - }, - "homepage": "https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme", - "files": [ - "dist", - "CHANGELOG.md" - ], - "author": { - "name": "Nate Moore", - "email": "nate@natemoo.re", - "url": "https://twitter.com/n_moore" - }, - "license": "MIT", - "keywords": [ - "ask", - "clack", - "cli", - "command-line", - "command", - "input", - "interact", - "interface", - "menu", - "prompt", - "prompts", - "stdin", - "ui" - ], - "packageManager": "pnpm@7.6.0", - "scripts": { - "build": "unbuild", - "prepack": "pnpm build" - }, - "dependencies": { - "@clack/core": "^0.3.2", - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - }, - "devDependencies": { - "is-unicode-supported": "^1.3.0" - }, - "bundledDependencies": [ - "is-unicode-supported" - ] -} + "name": "@clack/prompts", + "version": "0.6.3", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/natemoo-re/clack", + "directory": "packages/prompts" + }, + "bugs": { + "url": "https://github.com/natemoo-re/clack/issues" + }, + "homepage": "https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme", + "files": [ + "dist", + "CHANGELOG.md" + ], + "author": { + "name": "Nate Moore", + "email": "nate@natemoo.re", + "url": "https://twitter.com/n_moore" + }, + "license": "MIT", + "keywords": [ + "ask", + "clack", + "cli", + "command-line", + "command", + "input", + "interact", + "interface", + "menu", + "prompt", + "prompts", + "stdin", + "ui" + ], + "packageManager": "pnpm@7.6.0", + "scripts": { + "build": "unbuild", + "prepack": "pnpm build" + }, + "dependencies": { + "@clack/core": "^0.3.2", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + }, + "devDependencies": { + "is-unicode-supported": "^1.3.0" + }, + "bundledDependencies": [ + "is-unicode-supported" + ] +} \ No newline at end of file diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 3b342dd5..2784ca05 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -178,6 +178,7 @@ export interface SelectOptions[], Value> { message: string; options: Options; initialValue?: Value; + enableFilter?: boolean; } export const select = [], Value>( @@ -200,6 +201,7 @@ export const select = [], 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`; @@ -212,7 +214,7 @@ export const select = [], Value>( 'cancelled' )}\n${color.gray(S_BAR)}`; default: { - return `${title}${color.cyan(S_BAR)} ${this.options + return `${this.getFilterKey()}\n${title}${color.cyan(S_BAR)} ${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`; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 60721067..090be6f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -46,8 +46,22 @@ importers: devDependencies: jiti: 1.17.0 + examples/test: + specifiers: + '@clack/core': workspace:* + '@clack/prompts': workspace:* + jiti: ^1.17.0 + picocolors: ^1.0.0 + dependencies: + '@clack/core': link:../../packages/core + '@clack/prompts': link:../../packages/prompts + picocolors: 1.0.0 + devDependencies: + jiti: 1.17.1 + packages/core: specifiers: + fzf: ^0.5.2 picocolors: ^1.0.0 sisteransi: ^1.0.5 wrap-ansi: ^8.1.0 @@ -55,6 +69,7 @@ importers: picocolors: 1.0.0 sisteransi: 1.0.5 devDependencies: + fzf: 0.5.2 wrap-ansi: 8.1.0 packages/prompts: @@ -1491,6 +1506,10 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: true + /fzf/0.5.2: + resolution: {integrity: sha512-Tt4kuxLXFKHy8KT40zwsUPUkg1CrsgY25FxA2U/j/0WgEDCk3ddc/zLTCCcbSHX9FcKtLuVaDGtGE/STWC+j3Q==} + dev: true + /gensync/1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} From 5fbab45b1dd149dd1a5cb7bad943142da5863554 Mon Sep 17 00:00:00 2001 From: alexzhang1030 <1642114555@qq.com> Date: Fri, 26 May 2023 09:12:17 +0800 Subject: [PATCH 2/4] ui: render filterer --- packages/core/src/prompts/prompt.ts | 2 +- packages/core/src/prompts/select.ts | 4 ++-- packages/prompts/src/index.ts | 7 ++++++- 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/packages/core/src/prompts/prompt.ts b/packages/core/src/prompts/prompt.ts index 88685426..071eb70e 100644 --- a/packages/core/src/prompts/prompt.ts +++ b/packages/core/src/prompts/prompt.ts @@ -272,7 +272,7 @@ export default class Prompt { this._filterKey += key; } const filtered = fzf.find(this._filterKey); - this.emit('filtered', filtered); + this.emit('filter', filtered); }); } } diff --git a/packages/core/src/prompts/select.ts b/packages/core/src/prompts/select.ts index fcee3b6a..c726cf53 100644 --- a/packages/core/src/prompts/select.ts +++ b/packages/core/src/prompts/select.ts @@ -1,5 +1,5 @@ import Prompt, { PromptOptions } from './prompt'; -import { Fzf, FzfResultItem } from 'fzf'; +import { type FzfResultItem } from 'fzf'; interface SelectOptions extends PromptOptions> { options: T[]; @@ -43,7 +43,7 @@ export default class SelectPrompt extends Prompt { // For filter this.registerFilterer(this.options.map(({ value }) => value)); - this.on('filtered', (filtered: FzfResultItem[]) => { + this.on('filter', (filtered: FzfResultItem[]) => { this.cursor = 0; if (filtered.length) { this.options = filtered.map( diff --git a/packages/prompts/src/index.ts b/packages/prompts/src/index.ts index 2784ca05..a57346bb 100644 --- a/packages/prompts/src/index.ts +++ b/packages/prompts/src/index.ts @@ -205,6 +205,11 @@ export const select = [], Value>( 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')}`; @@ -214,7 +219,7 @@ export const select = [], Value>( 'cancelled' )}\n${color.gray(S_BAR)}`; default: { - return `${this.getFilterKey()}\n${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`; } From 67f62f7d5140d4721b85c6e59f7dd1fa7c580a07 Mon Sep 17 00:00:00 2001 From: alexzhang1030 <1642114555@qq.com> Date: Fri, 26 May 2023 09:30:09 +0800 Subject: [PATCH 3/4] fix: lint --- packages/prompts/package.json | 130 +++++++++++++++++----------------- 1 file changed, 65 insertions(+), 65 deletions(-) diff --git a/packages/prompts/package.json b/packages/prompts/package.json index 1a9f0361..52326461 100644 --- a/packages/prompts/package.json +++ b/packages/prompts/package.json @@ -1,66 +1,66 @@ { - "name": "@clack/prompts", - "version": "0.6.3", - "type": "module", - "main": "./dist/index.cjs", - "module": "./dist/index.mjs", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.cjs" - }, - "./package.json": "./package.json" - }, - "types": "./dist/index.d.ts", - "repository": { - "type": "git", - "url": "https://github.com/natemoo-re/clack", - "directory": "packages/prompts" - }, - "bugs": { - "url": "https://github.com/natemoo-re/clack/issues" - }, - "homepage": "https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme", - "files": [ - "dist", - "CHANGELOG.md" - ], - "author": { - "name": "Nate Moore", - "email": "nate@natemoo.re", - "url": "https://twitter.com/n_moore" - }, - "license": "MIT", - "keywords": [ - "ask", - "clack", - "cli", - "command-line", - "command", - "input", - "interact", - "interface", - "menu", - "prompt", - "prompts", - "stdin", - "ui" - ], - "packageManager": "pnpm@7.6.0", - "scripts": { - "build": "unbuild", - "prepack": "pnpm build" - }, - "dependencies": { - "@clack/core": "^0.3.2", - "picocolors": "^1.0.0", - "sisteransi": "^1.0.5" - }, - "devDependencies": { - "is-unicode-supported": "^1.3.0" - }, - "bundledDependencies": [ - "is-unicode-supported" - ] -} \ No newline at end of file + "name": "@clack/prompts", + "version": "0.6.3", + "type": "module", + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.cjs" + }, + "./package.json": "./package.json" + }, + "types": "./dist/index.d.ts", + "repository": { + "type": "git", + "url": "https://github.com/natemoo-re/clack", + "directory": "packages/prompts" + }, + "bugs": { + "url": "https://github.com/natemoo-re/clack/issues" + }, + "homepage": "https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme", + "files": [ + "dist", + "CHANGELOG.md" + ], + "author": { + "name": "Nate Moore", + "email": "nate@natemoo.re", + "url": "https://twitter.com/n_moore" + }, + "license": "MIT", + "keywords": [ + "ask", + "clack", + "cli", + "command-line", + "command", + "input", + "interact", + "interface", + "menu", + "prompt", + "prompts", + "stdin", + "ui" + ], + "packageManager": "pnpm@7.6.0", + "scripts": { + "build": "unbuild", + "prepack": "pnpm build" + }, + "dependencies": { + "@clack/core": "^0.3.2", + "picocolors": "^1.0.0", + "sisteransi": "^1.0.5" + }, + "devDependencies": { + "is-unicode-supported": "^1.3.0" + }, + "bundledDependencies": [ + "is-unicode-supported" + ] +} From 765a0cc68b1eb5606cd9336735169f6b97674402 Mon Sep 17 00:00:00 2001 From: Alex <49969959+alexzhang1030@users.noreply.github.com> Date: Fri, 26 May 2023 10:25:52 +0800 Subject: [PATCH 4/4] fix: lint Co-authored-by: Vasilii A <3757319+vsn4ik@users.noreply.github.com> --- packages/core/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/package.json b/packages/core/package.json index 0ebe0d06..3698a385 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -58,6 +58,6 @@ }, "devDependencies": { "wrap-ansi": "^8.1.0", - "fzf": "^0.5.2" + "fzf": "^0.5.2" } }