From a91ce5eff8531824925dbe0d0ac195268f42f964 Mon Sep 17 00:00:00 2001 From: lgl017 Date: Tue, 17 Mar 2026 15:30:51 +0800 Subject: [PATCH] path comp 1.tab can input selected value 2.put current dict into options 3.allow not exists --- examples/testpath/index.ts | 54 ++++++++++++++++++++++++ examples/testpath/package.json | 15 +++++++ examples/testpath/tsconfig.json | 3 ++ package.json | 1 + packages/prompts/src/autocomplete.ts | 6 +++ packages/prompts/src/path.ts | 62 +++++++++++++++++++--------- 6 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 examples/testpath/index.ts create mode 100644 examples/testpath/package.json create mode 100644 examples/testpath/tsconfig.json diff --git a/examples/testpath/index.ts b/examples/testpath/index.ts new file mode 100644 index 00000000..4912fd26 --- /dev/null +++ b/examples/testpath/index.ts @@ -0,0 +1,54 @@ +import { setTimeout } from 'node:timers/promises'; +import * as p from '@clack/prompts'; +import color from 'picocolors'; +import { resolve } from "node:path"; +import { statSync } from "node:fs"; + +function onCancel() { + p.cancel('Operation cancelled.'); + process.exit(0); +} + +async function main() { + console.clear(); + + await setTimeout(1000); + + const filePath = await p.path({ + message: "Please enter file path", + initialValue: resolve("."), + }) as string; + + p.log.info(filePath); + + const filePathFile = await p.path({ + message: "Please enter file path", + initialValue: resolve("."), + validate(val) { + if (!val || statSync(val).isDirectory()) { + return "Not a file"; + } + } + }) as string; + + p.log.info(filePathFile); + + const filePathDict = await p.path({ + message: "Please enter directory path", + initialValue: resolve("."), + directory: true, + }) as string; + + p.log.info(filePathDict); + + const filePathNotExists = await p.path({ + message: "Please enter file path", + initialValue: resolve("."), + directory: false, + exists: false, + }) as string; + + p.log.info(filePathNotExists); +} + +main().catch(console.error); diff --git a/examples/testpath/package.json b/examples/testpath/package.json new file mode 100644 index 00000000..bebab875 --- /dev/null +++ b/examples/testpath/package.json @@ -0,0 +1,15 @@ +{ + "name": "@example/testpath", + "private": true, + "version": "0.0.0", + "type": "module", + "dependencies": { + "jiti": "^1.17.0", + "@clack/prompts": "workspace:*", + "picocolors": "^1.0.0" + }, + "scripts": { + "start": "jiti ./index.ts" + }, + "devDependencies": {} +} diff --git a/examples/testpath/tsconfig.json b/examples/testpath/tsconfig.json new file mode 100644 index 00000000..4082f16a --- /dev/null +++ b/examples/testpath/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig.json" +} diff --git a/package.json b/package.json index b902cead..8bbb47a4 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "pnpm --filter \"@clack/*\" run build", "start": "pnpm run dev", "dev": "pnpm --filter @example/changesets run start", + "devpath": "pnpm --filter @example/testpath run start", "format": "biome check --write", "lint": "biome lint --write --unsafe", "types": "tsc --noEmit", diff --git a/packages/prompts/src/autocomplete.ts b/packages/prompts/src/autocomplete.ts index bb4dc69f..068ebaee 100644 --- a/packages/prompts/src/autocomplete.ts +++ b/packages/prompts/src/autocomplete.ts @@ -67,6 +67,8 @@ interface AutocompleteSharedOptions extends CommonOptions { * If not provided, a default filter that matches label, hint, and value is used. */ filter?: (search: string, option: Option) => boolean; + + initial?: (prompt: AutocompletePrompt>) => void; } export interface AutocompleteOptions extends AutocompleteSharedOptions { @@ -219,6 +221,10 @@ export const autocomplete = (opts: AutocompleteOptions) => { }, }); + if (opts.initial) { + opts.initial(prompt); + } + // Return the result or cancel symbol return prompt.prompt() as Promise; }; diff --git a/packages/prompts/src/path.ts b/packages/prompts/src/path.ts index 70b8618e..8a481ee6 100644 --- a/packages/prompts/src/path.ts +++ b/packages/prompts/src/path.ts @@ -1,5 +1,5 @@ import { existsSync, lstatSync, readdirSync } from 'node:fs'; -import { dirname, join } from 'node:path'; +import { dirname, join, sep } from 'node:path'; import { autocomplete } from './autocomplete.js'; import type { CommonOptions } from './common.js'; @@ -7,6 +7,7 @@ export interface PathOptions extends CommonOptions { root?: string; directory?: boolean; initialValue?: string; + exists?: boolean; message: string; validate?: (value: string | undefined) => string | Error | undefined; } @@ -18,13 +19,34 @@ export const path = (opts: PathOptions) => { ...opts, initialUserInput: opts.initialValue ?? opts.root ?? process.cwd(), maxItems: 5, + initial(prompt) { + prompt.on('key', (char, key) => { + if (key?.name === 'tab') { + if (prompt.selectedValues.length) { + let val = prompt.selectedValues[0]; + const stat = lstatSync(val); + if (stat.isDirectory() && !val.endsWith(sep)) { + val = val + sep; + } + prompt._clearUserInput(); + prompt._setUserInput(val, true); + } + } else if (key?.name === 'return') { + if (!opts.exists && !prompt.value) { + prompt.value = prompt.userInput; + } + } + }) + }, validate(value) { if (Array.isArray(value)) { // Shouldn't ever happen since we don't enable `multiple: true` return undefined; } if (!value) { - return 'Please select a path'; + if (opts.exists ?? true) { + return 'Please select a path'; + } } if (validate) { return validate(value); @@ -44,31 +66,31 @@ export const path = (opts: PathOptions) => { searchPath = dirname(userInput); } else { const stat = lstatSync(userInput); - if (stat.isDirectory() && (!opts.directory || userInput.endsWith('/'))) { + if (stat.isDirectory()) { searchPath = userInput; } else { searchPath = dirname(userInput); } } - // Strip trailing slash so startsWith matches the directory itself among its siblings - const prefix = - userInput.length > 1 && userInput.endsWith('/') ? userInput.slice(0, -1) : userInput; - - const items = readdirSync(searchPath) - .map((item) => { - const path = join(searchPath, item); - const stats = lstatSync(path); - return { - name: item, - path, - isDirectory: stats.isDirectory(), - }; - }) - .filter( - ({ path, isDirectory }) => path.startsWith(prefix) && (isDirectory || !opts.directory) + const items = [{ + name: searchPath, + path: searchPath, + isDirectory: true, + }].concat( + readdirSync(searchPath) + .map((item) => { + const path = join(searchPath, item); + const stats = lstatSync(path); + return { + name: item, + path, + isDirectory: stats.isDirectory(), + }; + }) + ).filter( + ({ path, isDirectory }) => path.startsWith(userInput) && (isDirectory || !opts.directory) ); - return items.map((item) => ({ value: item.path, }));