Skip to content
Open
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
54 changes: 54 additions & 0 deletions examples/testpath/index.ts
Original file line number Diff line number Diff line change
@@ -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);
15 changes: 15 additions & 0 deletions examples/testpath/package.json
Original file line number Diff line number Diff line change
@@ -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": {}
}
3 changes: 3 additions & 0 deletions examples/testpath/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
6 changes: 6 additions & 0 deletions packages/prompts/src/autocomplete.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ interface AutocompleteSharedOptions<Value> extends CommonOptions {
* If not provided, a default filter that matches label, hint, and value is used.
*/
filter?: (search: string, option: Option<Value>) => boolean;

initial?: (prompt: AutocompletePrompt<Option<Value>>) => void;
}

export interface AutocompleteOptions<Value> extends AutocompleteSharedOptions<Value> {
Expand Down Expand Up @@ -219,6 +221,10 @@ export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => {
},
});

if (opts.initial) {
opts.initial(prompt);
}

// Return the result or cancel symbol
return prompt.prompt() as Promise<Value | symbol>;
};
Expand Down
62 changes: 42 additions & 20 deletions packages/prompts/src/path.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
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';

export interface PathOptions extends CommonOptions {
root?: string;
directory?: boolean;
initialValue?: string;
exists?: boolean;
message: string;
validate?: (value: string | undefined) => string | Error | undefined;
}
Expand All @@ -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);
Expand All @@ -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,
}));
Expand Down
Loading