[READ-ONLY] Mirror of https://github.com/bombshell-dev/clack. Effortlessly build beautiful command-line apps bomb.sh/docs/clack/basics/getting-started/
cli command-line command-line-app node prompt prompts
5

Configure Feed

Select the types of activity you want to include in your feed.

feat: rework path, remove suggestion prompt (#335)

authored by

James Garbutt and committed by
GitHub
(Jun 3, 2025, 5:40 PM +0100) df4eea1c 53ebd43f

+502 -1288
+6
.changeset/short-taxis-cross.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + "@clack/core": minor 4 + --- 5 + 6 + Remove `suggestion` prompt and change `path` prompt to be an autocomplete prompt.
+1 -2
packages/core/src/index.ts
··· 1 - export type { ClackState as State, ValueWithCursorPart } from './types.js'; 1 + export type { ClackState as State } from './types.js'; 2 2 export type { ClackSettings } from './utils/settings.js'; 3 3 4 4 export { default as ConfirmPrompt } from './prompts/confirm.js'; ··· 10 10 export { default as SelectKeyPrompt } from './prompts/select-key.js'; 11 11 export { default as TextPrompt } from './prompts/text.js'; 12 12 export { default as AutocompletePrompt } from './prompts/autocomplete.js'; 13 - export { default as SuggestionPrompt } from './prompts/suggestion.js'; 14 13 export { block, isCancel, getColumns } from './utils/index.js'; 15 14 export { updateSettings, settings } from './utils/settings.js';
+31 -17
packages/core/src/prompts/autocomplete.ts
··· 46 46 47 47 interface AutocompleteOptions<T extends OptionLike> 48 48 extends PromptOptions<T['value'] | T['value'][], AutocompletePrompt<T>> { 49 - options: T[]; 49 + options: T[] | ((this: AutocompletePrompt<T>) => T[]); 50 50 filter?: FilterFunction<T>; 51 51 multiple?: boolean; 52 52 } ··· 54 54 export default class AutocompletePrompt<T extends OptionLike> extends Prompt< 55 55 T['value'] | T['value'][] 56 56 > { 57 - options: T[]; 58 57 filteredOptions: T[]; 59 58 multiple: boolean; 60 59 isNavigating = false; ··· 64 63 #cursor = 0; 65 64 #lastUserInput = ''; 66 65 #filterFn: FilterFunction<T>; 66 + #options: T[] | (() => T[]); 67 67 68 68 get cursor(): number { 69 69 return this.#cursor; ··· 81 81 return `${s1}${color.inverse(s2)}${s3.join('')}`; 82 82 } 83 83 84 + get options(): T[] { 85 + if (typeof this.#options === 'function') { 86 + return this.#options(); 87 + } 88 + return this.#options; 89 + } 90 + 84 91 constructor(opts: AutocompleteOptions<T>) { 85 92 super(opts); 86 93 87 - this.options = opts.options; 88 - this.filteredOptions = [...this.options]; 94 + this.#options = opts.options; 95 + const options = this.options; 96 + this.filteredOptions = [...options]; 89 97 this.multiple = opts.multiple === true; 90 98 this.#filterFn = opts.filter ?? defaultFilter; 91 99 let initialValues: unknown[] | undefined; ··· 103 111 104 112 if (initialValues) { 105 113 for (const selectedValue of initialValues) { 106 - const selectedIndex = this.options.findIndex((opt) => opt.value === selectedValue); 114 + const selectedIndex = options.findIndex((opt) => opt.value === selectedValue); 107 115 if (selectedIndex !== -1) { 108 116 this.toggleSelected(selectedValue); 109 117 this.#cursor = selectedIndex; ··· 113 121 114 122 this.focusedValue = this.options[this.#cursor]?.value; 115 123 116 - this.on('finalize', () => { 117 - if (!this.value) { 118 - this.value = normalisedValue(this.multiple, initialValues); 119 - } 120 - 121 - if (this.state === 'submit') { 122 - this.value = normalisedValue(this.multiple, this.selectedValues); 123 - } 124 - }); 125 - 126 124 this.on('key', (char, key) => this.#onKey(char, key)); 127 125 this.on('userInput', (value) => this.#onUserInputChanged(value)); 128 126 } ··· 141 139 #onKey(_char: string | undefined, key: Key): void { 142 140 const isUpKey = key.name === 'up'; 143 141 const isDownKey = key.name === 'down'; 142 + const isReturnKey = key.name === 'return'; 144 143 145 144 // Start navigation mode with up/down arrows 146 145 if (isUpKey || isDownKey) { ··· 153 152 this.selectedValues = [this.focusedValue]; 154 153 } 155 154 this.isNavigating = true; 155 + } else if (isReturnKey) { 156 + this.value = normalisedValue(this.multiple, this.selectedValues); 156 157 } else { 157 158 if (this.multiple) { 158 159 if ( ··· 169 170 } 170 171 } 171 172 } 173 + } 174 + 175 + deselectAll() { 176 + this.selectedValues = []; 172 177 } 173 178 174 179 toggleSelected(value: T['value']) { ··· 191 196 if (value !== this.#lastUserInput) { 192 197 this.#lastUserInput = value; 193 198 199 + const options = this.options; 200 + 194 201 if (value) { 195 - this.filteredOptions = this.options.filter((opt) => this.#filterFn(value, opt)); 202 + this.filteredOptions = options.filter((opt) => this.#filterFn(value, opt)); 196 203 } else { 197 - this.filteredOptions = [...this.options]; 204 + this.filteredOptions = [...options]; 198 205 } 199 206 this.#cursor = getCursorForValue(this.focusedValue, this.filteredOptions); 200 207 this.focusedValue = this.filteredOptions[this.#cursor]?.value; 208 + if (!this.multiple) { 209 + if (this.focusedValue !== undefined) { 210 + this.toggleSelected(this.focusedValue); 211 + } else { 212 + this.deselectAll(); 213 + } 214 + } 201 215 } 202 216 } 203 217 }
+5 -2
packages/core/src/prompts/prompt.ts
··· 13 13 export interface PromptOptions<TValue, Self extends Prompt<TValue>> { 14 14 render(this: Omit<Self, 'prompt'>): string | undefined; 15 15 initialValue?: any; 16 + initialUserInput?: string; 16 17 validate?: ((value: TValue | undefined) => string | Error | undefined) | undefined; 17 18 input?: Readable; 18 19 output?: Writable; ··· 25 26 protected output: Writable; 26 27 private _abortSignal?: AbortSignal; 27 28 28 - protected rl: ReadLine | undefined; 29 + private rl: ReadLine | undefined; 29 30 private opts: Omit<PromptOptions<TValue, Prompt<TValue>>, 'render' | 'input' | 'output'>; 30 31 private _render: (context: Omit<Prompt<TValue>, 'prompt'>) => string | undefined; 31 32 private _track = false; ··· 145 146 }); 146 147 this.rl.prompt(); 147 148 148 - this.emit('beforePrompt'); 149 + if (this.opts.initialUserInput !== undefined) { 150 + this._setUserInput(this.opts.initialUserInput, true); 151 + } 149 152 150 153 this.input.on('keypress', this.onKeypress); 151 154 setRawMode(this.input, true);
-126
packages/core/src/prompts/suggestion.ts
··· 1 - import type { Key } from 'node:readline'; 2 - import type { ValueWithCursorPart } from '../types.js'; 3 - import Prompt, { type PromptOptions } from './prompt.js'; 4 - 5 - interface SuggestionOptions extends PromptOptions<string, SuggestionPrompt> { 6 - suggest: (value: string) => Array<string>; 7 - initialValue: string; 8 - } 9 - 10 - export default class SuggestionPrompt extends Prompt<string> { 11 - protected suggest: (value: string) => Array<string>; 12 - private selectionIndex = 0; 13 - private nextItems: Array<string> = []; 14 - 15 - constructor(opts: SuggestionOptions) { 16 - super(opts); 17 - 18 - this.suggest = opts.suggest; 19 - this.getNextItems(); 20 - this.selectionIndex = 0; 21 - 22 - this.on('beforePrompt', () => { 23 - if (opts.initialValue !== undefined) { 24 - this._setUserInput(opts.initialValue, true); 25 - } 26 - }); 27 - this.on('cursor', (key) => { 28 - switch (key) { 29 - case 'up': 30 - this.selectionIndex = Math.max( 31 - 0, 32 - this.selectionIndex === 0 ? this.nextItems.length - 1 : this.selectionIndex - 1 33 - ); 34 - this.value = this.nextItems[this.selectionIndex]; 35 - break; 36 - case 'down': 37 - this.selectionIndex = 38 - this.nextItems.length === 0 ? 0 : (this.selectionIndex + 1) % this.nextItems.length; 39 - this.value = this.nextItems[this.selectionIndex]; 40 - break; 41 - } 42 - }); 43 - this.on('key', (_key, info) => { 44 - const nextItem = this.nextItems[this.selectionIndex]; 45 - if (info.name === 'tab' && nextItem !== undefined) { 46 - const delta = nextItem.substring(this.userInput.length); 47 - // TODO (43081j): this means the selected value won't show up until we 48 - // later choose another value. probably shouldn't set `value` until 49 - // finalize tbh 50 - this.value = nextItem; 51 - this.rl?.write(delta); 52 - this._cursor = this.rl?.cursor ?? 0; 53 - this.selectionIndex = 0; 54 - this._setUserInput(this.userInput + delta); 55 - } 56 - }); 57 - this.on('userInput', () => { 58 - if (this.value !== this.userInput) { 59 - this.value = this.userInput; 60 - } 61 - 62 - this.getNextItems(); 63 - }); 64 - } 65 - 66 - get displayValue(): Array<ValueWithCursorPart> { 67 - const result: Array<ValueWithCursorPart> = []; 68 - if (this._cursor > 0) { 69 - result.push({ 70 - text: this.userInput.substring(0, this._cursor), 71 - type: 'value', 72 - }); 73 - } 74 - if (this._cursor < this.userInput.length) { 75 - result.push({ 76 - text: this.userInput.substring(this._cursor, this._cursor + 1), 77 - type: 'cursor_on_value', 78 - }); 79 - const left = this.userInput.substring(this._cursor + 1); 80 - if (left.length > 0) { 81 - result.push({ 82 - text: left, 83 - type: 'value', 84 - }); 85 - } 86 - if (this.suggestion.length > 0) { 87 - result.push({ 88 - text: this.suggestion, 89 - type: 'suggestion', 90 - }); 91 - } 92 - return result; 93 - } 94 - if (this.suggestion.length === 0) { 95 - result.push({ 96 - text: '\u00A0', 97 - type: 'cursor_on_value', 98 - }); 99 - return result; 100 - } 101 - result.push( 102 - { 103 - text: this.suggestion[0], 104 - type: 'cursor_on_suggestion', 105 - }, 106 - { 107 - text: this.suggestion.substring(1), 108 - type: 'suggestion', 109 - } 110 - ); 111 - return result; 112 - } 113 - 114 - get suggestion(): string { 115 - return this.nextItems[this.selectionIndex]?.substring(this.userInput.length) ?? ''; 116 - } 117 - 118 - private getNextItems(): void { 119 - this.nextItems = this.suggest(this.userInput).filter((item) => { 120 - return item.startsWith(this.userInput) && item !== this.value; 121 - }); 122 - if (this.selectionIndex > this.nextItems.length) { 123 - this.selectionIndex = 0; 124 - } 125 - } 126 - }
+4 -6
packages/core/src/prompts/text.ts
··· 23 23 return this._cursor; 24 24 } 25 25 constructor(opts: TextOptions) { 26 - super(opts); 27 - 28 - this.on('beforePrompt', () => { 29 - if (opts.initialValue !== undefined) { 30 - this._setUserInput(opts.initialValue, true); 31 - } 26 + super({ 27 + ...opts, 28 + initialUserInput: opts.initialUserInput ?? opts.initialValue, 32 29 }); 30 + 33 31 this.on('userInput', (input) => { 34 32 this._setValue(input); 35 33 });
-8
packages/core/src/types.ts
··· 23 23 finalize: () => void; 24 24 beforePrompt: () => void; 25 25 } 26 - 27 - /** 28 - * Display a value 29 - */ 30 - export interface ValueWithCursorPart { 31 - text: string; 32 - type: 'value' | 'cursor_on_value' | 'suggestion' | 'cursor_on_suggestion'; 33 - }
-226
packages/core/test/prompts/suggestion.test.ts
··· 1 - import color from 'picocolors'; 2 - import { cursor } from 'sisteransi'; 3 - import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 - import { default as SelectPrompt } from '../../src/prompts/select.js'; 5 - import { default as SuggestionPrompt } from '../../src/prompts/suggestion.js'; 6 - import { MockReadable } from '../mock-readable.js'; 7 - import { MockWritable } from '../mock-writable.js'; 8 - 9 - describe(SuggestionPrompt.name, () => { 10 - let input: MockReadable; 11 - let output: MockWritable; 12 - 13 - beforeEach(() => { 14 - input = new MockReadable(); 15 - output = new MockWritable(); 16 - }); 17 - 18 - afterEach(() => { 19 - vi.restoreAllMocks(); 20 - }); 21 - 22 - describe('displayValue getter return all parts/cases', () => { 23 - test('no suggestion, cursor at the end', () => { 24 - const instance = new SuggestionPrompt({ 25 - input, 26 - output, 27 - suggest: () => [], 28 - initialValue: 'Lorem ipsum', 29 - render: () => 'Lorem ipsum', 30 - }); 31 - // leave the promise hanging since we don't want to submit in this test 32 - instance.prompt(); 33 - expect(instance.displayValue).to.deep.equal([ 34 - { text: 'Lorem ipsum', type: 'value' }, 35 - { text: '\u00a0', type: 'cursor_on_value' }, 36 - ]); 37 - }); 38 - 39 - test('no suggestion, cursor at the start', () => { 40 - const instance = new SuggestionPrompt({ 41 - input, 42 - output, 43 - suggest: () => [], 44 - initialValue: 'Lorem', 45 - render: () => 'Lorem', 46 - }); 47 - // leave the promise hanging since we don't want to submit in this test 48 - instance.prompt(); 49 - for (let index = 0; index < 5; index++) input.emit('keypress', '', { name: 'left' }); 50 - expect(instance.displayValue).to.deep.equal([ 51 - { text: 'L', type: 'cursor_on_value' }, 52 - { text: 'orem', type: 'value' }, 53 - ]); 54 - }); 55 - test('no suggestion, cursor in the middle', () => { 56 - const instance = new SuggestionPrompt({ 57 - input, 58 - output, 59 - suggest: () => [], 60 - initialValue: 'Lorem', 61 - render: () => 'Lorem', 62 - }); 63 - // leave the promise hanging since we don't want to submit in this test 64 - instance.prompt(); 65 - for (let index = 0; index < 3; index++) input.emit('keypress', '', { name: 'left' }); 66 - expect(instance.displayValue).to.deep.equal([ 67 - { text: 'Lo', type: 'value' }, 68 - { text: 'r', type: 'cursor_on_value' }, 69 - { text: 'em', type: 'value' }, 70 - ]); 71 - }); 72 - test('no suggestion, cursor on the last letter', () => { 73 - const instance = new SuggestionPrompt({ 74 - input, 75 - output, 76 - suggest: () => [], 77 - initialValue: 'Lorem', 78 - render: () => 'Lorem', 79 - }); 80 - // leave the promise hanging since we don't want to submit in this test 81 - instance.prompt(); 82 - input.emit('keypress', '', { name: 'left' }); 83 - expect(instance.displayValue).to.deep.equal([ 84 - { text: 'Lore', type: 'value' }, 85 - { text: 'm', type: 'cursor_on_value' }, 86 - ]); 87 - }); 88 - test('with suggestion, cursor at the end', () => { 89 - const instance = new SuggestionPrompt({ 90 - input, 91 - output, 92 - suggest: () => ['Lorem ipsum dolor sit amet, consectetur adipiscing elit'], 93 - initialValue: 'Lorem ipsum dolor sit amet', 94 - render: () => 'Lorem ipsum dolor sit amet', 95 - }); 96 - // leave the promise hanging since we don't want to submit in this test 97 - instance.prompt(); 98 - expect(instance.displayValue).to.deep.equal([ 99 - { text: 'Lorem ipsum dolor sit amet', type: 'value' }, 100 - { text: ',', type: 'cursor_on_suggestion' }, 101 - { text: ' consectetur adipiscing elit', type: 'suggestion' }, 102 - ]); 103 - }); 104 - test('with suggestion, cursor not at the end', () => { 105 - const instance = new SuggestionPrompt({ 106 - input, 107 - output, 108 - suggest: () => ['Lorem ipsum dolor sit amet, consectetur adipiscing elit'], 109 - initialValue: 'Lorem ipsum dolor sit amet', 110 - render: () => 'Lorem ipsum dolor sit amet', 111 - }); 112 - // leave the promise hanging since we don't want to submit in this test 113 - instance.prompt(); 114 - for (let index = 0; index < 3; index++) input.emit('keypress', '', { name: 'left' }); 115 - expect(instance.displayValue).to.deep.equal([ 116 - { text: 'Lorem ipsum dolor sit a', type: 'value' }, 117 - { text: 'm', type: 'cursor_on_value' }, 118 - { text: 'et', type: 'value' }, 119 - { text: ', consectetur adipiscing elit', type: 'suggestion' }, 120 - ]); 121 - }); 122 - }); 123 - describe('navigate suggestion', () => { 124 - test('the default is the first suggestion', () => { 125 - const instance = new SuggestionPrompt({ 126 - input, 127 - output, 128 - suggest: () => ['foobar', 'foobaz'], 129 - initialValue: 'foo', 130 - render: () => 'foo', 131 - }); 132 - // leave the promise hanging since we don't want to submit in this test 133 - instance.prompt(); 134 - expect(instance.suggestion).to.be.equal('bar'); 135 - }); 136 - test('down display next suggestion', () => { 137 - const instance = new SuggestionPrompt({ 138 - input, 139 - output, 140 - suggest: () => ['foobar', 'foobaz'], 141 - initialValue: 'foo', 142 - render: () => 'foo', 143 - }); 144 - // leave the promise hanging since we don't want to submit in this test 145 - instance.prompt(); 146 - input.emit('keypress', '', { name: 'down' }); 147 - 148 - expect(instance.suggestion).to.be.equal('baz'); 149 - }); 150 - test('suggestions loops (down)', () => { 151 - const instance = new SuggestionPrompt({ 152 - input, 153 - output, 154 - suggest: () => ['foobar', 'foobaz'], 155 - initialValue: 'foo', 156 - render: () => 'foo', 157 - }); 158 - // leave the promise hanging since we don't want to submit in this test 159 - instance.prompt(); 160 - expect(instance.suggestion).to.be.equal('bar'); 161 - input.emit('keypress', '', { name: 'down' }); 162 - expect(instance.suggestion).to.be.equal('baz'); 163 - input.emit('keypress', '', { name: 'down' }); 164 - expect(instance.suggestion).to.be.equal('bar'); 165 - }); 166 - 167 - test('suggestions loops (up)', () => { 168 - const instance = new SuggestionPrompt({ 169 - input, 170 - output, 171 - suggest: () => ['foobar', 'foobaz'], 172 - initialValue: 'foo', 173 - render: () => 'foo', 174 - }); 175 - // leave the promise hanging since we don't want to submit in this test 176 - instance.prompt(); 177 - expect(instance.suggestion).to.be.equal('bar'); 178 - input.emit('keypress', '', { name: 'up' }); 179 - expect(instance.suggestion).to.be.equal('baz'); 180 - input.emit('keypress', '', { name: 'up' }); 181 - expect(instance.suggestion).to.be.equal('bar'); 182 - }); 183 - }); 184 - test('tab validate suggestion', () => { 185 - const instance = new SuggestionPrompt({ 186 - input, 187 - output, 188 - suggest: () => ['foobar', 'foobaz'], 189 - initialValue: 'foo', 190 - render: () => 'foo', 191 - }); 192 - // leave the promise hanging since we don't want to submit in this test 193 - instance.prompt(); 194 - expect(instance.suggestion).to.be.equal('bar'); 195 - expect(instance.value).to.be.equal('foo'); 196 - input.emit('keypress', '', { name: 'tab' }); 197 - expect(instance.suggestion).to.be.equal(''); 198 - expect(instance.value).to.be.equal('foobar'); 199 - }); 200 - describe('suggestion are filtered', () => { 201 - test("suggestion that don't match (begin) at not displayed", () => { 202 - const instance = new SuggestionPrompt({ 203 - input, 204 - output, 205 - suggest: () => ['foobar', 'foobaz', 'hello world'], 206 - initialValue: 'foo', 207 - render: () => 'foo', 208 - }); 209 - // leave the promise hanging since we don't want to submit in this test 210 - instance.prompt(); 211 - expect((instance as unknown as { nextItems: Array<string> }).nextItems.length).to.be.equal(2); 212 - }); 213 - test('empty suggestions are removed', () => { 214 - const instance = new SuggestionPrompt({ 215 - input, 216 - output, 217 - suggest: () => ['foo'], 218 - initialValue: 'foo', 219 - render: () => 'foo', 220 - }); 221 - // leave the promise hanging since we don't want to submit in this test 222 - instance.prompt(); 223 - expect((instance as unknown as { nextItems: Array<string> }).nextItems.length).to.be.equal(0); 224 - }); 225 - }); 226 - });
+21 -4
packages/prompts/src/autocomplete.ts
··· 49 49 /** 50 50 * Available options for the autocomplete prompt. 51 51 */ 52 - options: Option<Value>[]; 52 + options: Option<Value>[] | ((this: AutocompletePrompt<Option<Value>>) => Option<Value>[]); 53 53 /** 54 54 * Maximum number of items to display at once. 55 55 */ ··· 58 58 * Placeholder text to display when no input is provided. 59 59 */ 60 60 placeholder?: string; 61 + /** 62 + * Validates the value 63 + */ 64 + validate?: (value: Value | Value[] | undefined) => string | Error | undefined; 61 65 } 62 66 63 67 export interface AutocompleteOptions<Value> extends AutocompleteSharedOptions<Value> { ··· 65 69 * The initial selected value. 66 70 */ 67 71 initialValue?: Value; 72 + /** 73 + * The initial user input 74 + */ 75 + initialUserInput?: string; 68 76 } 69 77 70 78 export const autocomplete = <Value>(opts: AutocompleteOptions<Value>) => { 71 79 const prompt = new AutocompletePrompt({ 72 80 options: opts.options, 73 81 initialValue: opts.initialValue ? [opts.initialValue] : undefined, 82 + initialUserInput: opts.initialUserInput, 74 83 filter: (search: string, opt: Option<Value>) => { 75 84 return getFilteredOption(search, opt); 76 85 }, 77 86 input: opts.input, 78 87 output: opts.output, 88 + validate: opts.validate, 79 89 render() { 80 90 // Title and message display 81 91 const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 82 92 const userInput = this.userInput; 83 93 const valueAsString = String(this.value ?? ''); 94 + const options = this.options; 84 95 const placeholder = opts.placeholder; 85 96 const showPlaceholder = valueAsString === '' && placeholder !== undefined; 86 97 ··· 88 99 switch (this.state) { 89 100 case 'submit': { 90 101 // Show selected value 91 - const selected = getSelectedOptions(this.selectedValues, this.options); 102 + const selected = getSelectedOptions(this.selectedValues, options); 92 103 const label = selected.length > 0 ? selected.map(getLabel).join(', ') : ''; 93 104 return `${title}${color.gray(S_BAR)} ${color.dim(label)}`; 94 105 } ··· 106 117 107 118 // Show match count if filtered 108 119 const matches = 109 - this.filteredOptions.length !== this.options.length 120 + this.filteredOptions.length !== options.length 110 121 ? color.dim( 111 122 ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` 112 123 ) ··· 147 158 ? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`] 148 159 : []; 149 160 161 + const validationError = 162 + this.state === 'error' ? [`${color.yellow(S_BAR)} ${color.yellow(this.error)}`] : []; 163 + 150 164 // Return the formatted prompt 151 165 return [ 152 166 title, 153 167 `${color.cyan(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`, 154 168 ...noResults, 169 + ...validationError, 155 170 ...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`), 156 171 `${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`, 157 172 `${color.cyan(S_BAR_END)}`, ··· 232 247 ? color.dim(showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode 233 248 : this.userInputWithCursor; 234 249 250 + const options = this.options; 251 + 235 252 const matches = 236 - this.filteredOptions.length !== opts.options.length 253 + this.filteredOptions.length !== options.length 237 254 ? color.dim( 238 255 ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` 239 256 )
+3 -2
packages/prompts/src/group-multi-select.ts
··· 74 74 const unselectedCheckbox = isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ''; 75 75 return `${spacingPrefix}${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; 76 76 }; 77 + const required = opts.required ?? true; 77 78 78 79 return new GroupMultiSelectPrompt({ 79 80 options: opts.options, 80 81 input: opts.input, 81 82 output: opts.output, 82 83 initialValues: opts.initialValues, 83 - required: opts.required ?? true, 84 + required, 84 85 cursorAt: opts.cursorAt, 85 86 selectableGroups, 86 87 validate(selected: Value[] | undefined) { 87 - if (this.required && (selected === undefined || selected.length === 0)) 88 + if (required && (selected === undefined || selected.length === 0)) 88 89 return `Please select at least one option.\n${color.reset( 89 90 color.dim( 90 91 `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray(
-1
packages/prompts/src/index.ts
··· 17 17 export * from './select.js'; 18 18 export * from './spinner.js'; 19 19 export * from './stream.js'; 20 - export * from './suggestion.js'; 21 20 export * from './task.js'; 22 21 export * from './task-log.js'; 23 22 export * from './text.js';
+3 -2
packages/prompts/src/multi-select.ts
··· 49 49 } 50 50 return `${color.dim(S_CHECKBOX_INACTIVE)} ${color.dim(label)}`; 51 51 }; 52 + const required = opts.required ?? true; 52 53 53 54 return new MultiSelectPrompt({ 54 55 options: opts.options, 55 56 input: opts.input, 56 57 output: opts.output, 57 58 initialValues: opts.initialValues, 58 - required: opts.required ?? true, 59 + required, 59 60 cursorAt: opts.cursorAt, 60 61 validate(selected: Value[] | undefined) { 61 - if (this.required && (selected === undefined || selected.length === 0)) 62 + if (required && (selected === undefined || selected.length === 0)) 62 63 return `Please select at least one option.\n${color.reset( 63 64 color.dim( 64 65 `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray(
+45 -13
packages/prompts/src/path.ts
··· 1 1 import { existsSync, lstatSync, readdirSync } from 'node:fs'; 2 - import { join } from 'node:path'; 3 - import { dirname } from 'knip/dist/util/path.js'; 2 + import { dirname, join } from 'node:path'; 3 + import { autocomplete } from './autocomplete.js'; 4 4 import type { CommonOptions } from './common.js'; 5 - import { suggestion } from './suggestion.js'; 6 5 7 6 export interface PathOptions extends CommonOptions { 8 7 root?: string; ··· 13 12 } 14 13 15 14 export const path = (opts: PathOptions) => { 16 - return suggestion({ 15 + const validate = opts.validate; 16 + 17 + return autocomplete({ 17 18 ...opts, 18 - initialValue: opts.initialValue ?? opts.root ?? process.cwd(), 19 - suggest: (value: string) => { 19 + initialUserInput: opts.initialValue ?? opts.root ?? process.cwd(), 20 + maxItems: 5, 21 + validate(value) { 22 + if (Array.isArray(value)) { 23 + // Shouldn't ever happen since we don't enable `multiple: true` 24 + return undefined; 25 + } 26 + if (!value) { 27 + return 'Please select a path'; 28 + } 29 + if (validate) { 30 + return validate(value); 31 + } 32 + return undefined; 33 + }, 34 + options() { 35 + const userInput = this.userInput; 36 + if (userInput === '') { 37 + return []; 38 + } 39 + 20 40 try { 21 - const searchPath = !existsSync(value) ? dirname(value) : value; 22 - if (!lstatSync(searchPath).isDirectory()) { 23 - return []; 41 + let searchPath: string; 42 + 43 + if (!existsSync(userInput)) { 44 + searchPath = dirname(userInput); 45 + } else { 46 + const stat = lstatSync(userInput); 47 + if (stat.isDirectory()) { 48 + searchPath = userInput; 49 + } else { 50 + searchPath = dirname(userInput); 51 + } 24 52 } 53 + 25 54 const items = readdirSync(searchPath) 26 55 .map((item) => { 27 56 const path = join(searchPath, item); ··· 32 61 isDirectory: stats.isDirectory(), 33 62 }; 34 63 }) 35 - .filter(({ path }) => path.startsWith(value)); 36 - return ((opts.directory ?? false) ? items.filter((item) => item.isDirectory) : items).map( 37 - ({ path }) => path 38 - ); 64 + .filter( 65 + ({ path, isDirectory }) => 66 + path.startsWith(userInput) && (opts.directory || !isDirectory) 67 + ); 68 + return items.map((item) => ({ 69 + value: item.path, 70 + })); 39 71 } catch (e) { 40 72 return []; 41 73 }
-50
packages/prompts/src/suggestion.ts
··· 1 - import { SuggestionPrompt, type ValueWithCursorPart } from '@clack/core'; 2 - import color from 'picocolors'; 3 - import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; 4 - 5 - export interface SuggestionOptions extends CommonOptions { 6 - initialValue?: string; 7 - message: string; 8 - validate?: (value: string | undefined) => string | Error | undefined; 9 - suggest: (value: string) => Array<string>; 10 - } 11 - 12 - export const suggestion = (opts: SuggestionOptions) => { 13 - return new SuggestionPrompt({ 14 - initialValue: opts.initialValue ?? '', 15 - output: opts.output, 16 - input: opts.input, 17 - validate: opts.validate, 18 - suggest: opts.suggest, 19 - render() { 20 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 21 - const value = this.displayValue.reduce((text: string, line: ValueWithCursorPart) => { 22 - switch (line.type) { 23 - case 'value': 24 - return text + line.text; 25 - case 'cursor_on_value': 26 - return text + color.inverse(line.text); 27 - case 'suggestion': 28 - return text + color.gray(line.text); 29 - case 'cursor_on_suggestion': 30 - return text + color.inverse(color.gray(line.text)); 31 - } 32 - }, ''); 33 - 34 - switch (this.state) { 35 - case 'error': 36 - return `${title.trim()}\n${color.yellow(S_BAR)} ${value}\n${color.yellow( 37 - S_BAR_END 38 - )} ${color.yellow(this.error)}\n`; 39 - case 'submit': 40 - return `${title}${color.gray(S_BAR)} ${color.dim(this.value)}`; 41 - case 'cancel': 42 - return `${title}${color.gray(S_BAR)} ${color.strikethrough( 43 - color.dim(this.value ?? '') 44 - )}${this.value?.trim() ? `\n${color.gray(S_BAR)}` : ''}`; 45 - default: 46 - return `${title}${color.cyan(S_BAR)} ${value}\n${color.cyan(S_BAR_END)}\n`; 47 - } 48 - }, 49 - }).prompt() as Promise<string | symbol>; 50 - };
+1 -1
packages/prompts/test/__snapshots__/autocomplete.test.ts.snap
··· 173 173 "<cursor.down count=1>", 174 174 "<erase.down>", 175 175 "◇ Select a fruit 176 - │ Apple", 176 + │", 177 177 " 178 178 ", 179 179 "<cursor.show>",
+348 -200
packages/prompts/test/__snapshots__/path.test.ts.snap
··· 5 5 "<cursor.hide>", 6 6 "│ 7 7 ◆ foo 8 - │ /tmp/foo 9 - └ 10 - ", 11 - "<cursor.backward count=999><cursor.up count=4>", 8 + 9 + │ Search: /tmp/█ 10 + │ ● /tmp/bar 11 + │ ○ /tmp/root.zip 12 + │ ↑/↓ to select • Enter: confirm • Type: to search 13 + └", 14 + "<cursor.backward count=999><cursor.up count=7>", 12 15 "<cursor.down count=1>", 13 16 "<erase.down>", 14 17 "■ foo 15 - │ /tmp/ 16 - │", 18 + │ /tmp/", 17 19 " 18 20 ", 19 21 "<cursor.show>", 20 22 ] 21 23 `; 22 24 23 - exports[`text (isCI = false) > initialValue sets the value 1`] = ` 25 + exports[`text (isCI = false) > cannot submit unknown value 1`] = ` 24 26 [ 25 27 "<cursor.hide>", 26 28 "│ 27 29 ◆ foo 28 - │ /tmp/bar  29 - └ 30 - ", 31 - "<cursor.backward count=999><cursor.up count=4>", 30 + 31 + │ Search: /tmp/█ 32 + │ ● /tmp/bar 33 + │ ○ /tmp/root.zip 34 + │ ↑/↓ to select • Enter: confirm • Type: to search 35 + └", 36 + "<cursor.backward count=999><cursor.up count=7>", 37 + "<cursor.down count=3>", 38 + "<erase.down>", 39 + "│ Search: /tmp/_█ 40 + │ No matches found 41 + │ ↑/↓ to select • Enter: confirm • Type: to search 42 + └", 43 + "<cursor.backward count=999><cursor.up count=6>", 44 + "<cursor.down count=1>", 45 + "<erase.down>", 46 + "▲ foo 47 + 48 + │ Search: /tmp/_█ 49 + │ No matches found 50 + │ Please select a path 51 + │ ↑/↓ to select • Enter: confirm • Type: to search 52 + └", 53 + "<cursor.backward count=999><cursor.up count=7>", 54 + "<cursor.down count=1>", 55 + "<erase.down>", 56 + "◆ foo 57 + 58 + │ Search: /tmp/█ 59 + │ ● /tmp/bar 60 + │ ○ /tmp/root.zip 61 + │ ↑/↓ to select • Enter: confirm • Type: to search 62 + └", 63 + "<cursor.backward count=999><cursor.up count=7>", 64 + "<cursor.down count=3>", 65 + "<erase.down>", 66 + "│ Search: /tmp/b█ 67 + │ ● /tmp/bar 68 + │ ↑/↓ to select • Enter: confirm • Type: to search 69 + └", 70 + "<cursor.backward count=999><cursor.up count=6>", 32 71 "<cursor.down count=1>", 33 72 "<erase.down>", 34 73 "◇ foo ··· 39 78 ] 40 79 `; 41 80 42 - exports[`text (isCI = false) > renders and apply (<tab>) suggestion 1`] = ` 81 + exports[`text (isCI = false) > initialValue sets the value 1`] = ` 43 82 [ 44 83 "<cursor.hide>", 45 84 "│ 46 85 ◆ foo 47 - │ /tmp/foo 48 - └ 49 - ", 50 - "<cursor.backward count=999><cursor.up count=4>", 51 - "<cursor.down count=2>", 52 - "<erase.line><cursor.left count=1>", 53 - "│ /tmp/foo/bar.txt", 54 - "<cursor.down count=2>", 55 - "<cursor.backward count=999><cursor.up count=4>", 86 + 87 + │ Search: /tmp/bar█ 88 + │ ● /tmp/bar 89 + │ ↑/↓ to select • Enter: confirm • Type: to search 90 + └", 91 + "<cursor.backward count=999><cursor.up count=6>", 56 92 "<cursor.down count=1>", 57 93 "<erase.down>", 58 94 "◇ foo 59 - │ /tmp/foo", 95 + │ /tmp/bar", 60 96 " 61 97 ", 62 98 "<cursor.show>", ··· 68 104 "<cursor.hide>", 69 105 "│ 70 106 ◆ foo 71 - │ /tmp/foo 72 - └ 73 - ", 74 - "<cursor.backward count=999><cursor.up count=4>", 75 - "<cursor.down count=2>", 76 - "<erase.line><cursor.left count=1>", 77 - "│ /tmp/x ", 78 - "<cursor.down count=2>", 79 - "<cursor.backward count=999><cursor.up count=4>", 80 - "<cursor.down count=2>", 107 + 108 + │ Search: /tmp/█ 109 + │ ● /tmp/bar 110 + │ ○ /tmp/root.zip 111 + │ ↑/↓ to select • Enter: confirm • Type: to search 112 + └", 113 + "<cursor.backward count=999><cursor.up count=7>", 114 + "<cursor.down count=3>", 115 + "<erase.down>", 116 + "│ Search: /tmp/x█ 117 + │ No matches found 118 + │ ↑/↓ to select • Enter: confirm • Type: to search 119 + └", 120 + "<cursor.backward count=999><cursor.up count=6>", 121 + "<cursor.down count=3>", 81 122 "<erase.line><cursor.left count=1>", 82 - "│ /tmp/xy ", 83 - "<cursor.down count=2>", 84 - "<cursor.backward count=999><cursor.up count=4>", 123 + "│ Search: /tmp/xy█", 124 + "<cursor.down count=3>", 125 + "<cursor.backward count=999><cursor.up count=6>", 85 126 "<cursor.down count=1>", 86 127 "<erase.down>", 87 128 "■ foo 88 - │ /tmp/xy 89 - │", 129 + │ /tmp/xy", 90 130 " 91 131 ", 92 132 "<cursor.show>", ··· 98 138 "<cursor.hide>", 99 139 "│ 100 140 ◆ foo 101 - │ /tmp/foo 102 - └ 103 - ", 104 - "<cursor.backward count=999><cursor.up count=4>", 141 + 142 + │ Search: /tmp/█ 143 + │ ● /tmp/bar 144 + │ ○ /tmp/root.zip 145 + │ ↑/↓ to select • Enter: confirm • Type: to search 146 + └", 147 + "<cursor.backward count=999><cursor.up count=7>", 105 148 "<cursor.down count=1>", 106 149 "<erase.down>", 107 150 "◇ foo 108 - │ /tmp/", 151 + │ /tmp/bar", 109 152 " 110 153 ", 111 154 "<cursor.show>", ··· 117 160 "<cursor.hide>", 118 161 "│ 119 162 ◆ foo 120 - │ /tmp/foo 121 - └ 122 - ", 123 - "<cursor.backward count=999><cursor.up count=4>", 124 - "<cursor.down count=2>", 125 - "<erase.line><cursor.left count=1>", 126 - "│ /tmp/x ", 127 - "<cursor.down count=2>", 128 - "<cursor.backward count=999><cursor.up count=4>", 129 - "<cursor.down count=2>", 163 + 164 + │ Search: /tmp/█ 165 + │ ● /tmp/bar 166 + │ ○ /tmp/root.zip 167 + │ ↑/↓ to select • Enter: confirm • Type: to search 168 + └", 169 + "<cursor.backward count=999><cursor.up count=7>", 170 + "<cursor.down count=3>", 171 + "<erase.down>", 172 + "│ Search: /tmp/b█ 173 + │ ● /tmp/bar 174 + │ ↑/↓ to select • Enter: confirm • Type: to search 175 + └", 176 + "<cursor.backward count=999><cursor.up count=6>", 177 + "<cursor.down count=3>", 130 178 "<erase.line><cursor.left count=1>", 131 - "│ /tmp/xy ", 132 - "<cursor.down count=2>", 133 - "<cursor.backward count=999><cursor.up count=4>", 179 + "│ Search: /tmp/ba█", 180 + "<cursor.down count=3>", 181 + "<cursor.backward count=999><cursor.up count=6>", 134 182 "<cursor.down count=1>", 135 183 "<erase.down>", 136 184 "◇ foo 137 - │ /tmp/xy", 185 + │ /tmp/bar", 138 186 " 139 187 ", 140 188 "<cursor.show>", ··· 146 194 "<cursor.hide>", 147 195 "│ 148 196 ◆ foo 149 - │ /tmp/foo 150 - └ 151 - ", 152 - "<cursor.backward count=999><cursor.up count=4>", 153 - "<cursor.down count=2>", 154 - "<erase.line><cursor.left count=1>", 155 - "│ /tmp/b ", 156 - "<cursor.down count=2>", 157 - "<cursor.backward count=999><cursor.up count=4>", 197 + 198 + │ Search: /tmp/█ 199 + │ ● /tmp/bar 200 + │ ○ /tmp/root.zip 201 + │ ↑/↓ to select • Enter: confirm • Type: to search 202 + └", 203 + "<cursor.backward count=999><cursor.up count=7>", 204 + "<cursor.down count=3>", 205 + "<erase.down>", 206 + "│ Search: /tmp/r█ 207 + │ ● /tmp/root.zip 208 + │ ↑/↓ to select • Enter: confirm • Type: to search 209 + └", 210 + "<cursor.backward count=999><cursor.up count=6>", 158 211 "<cursor.down count=1>", 159 212 "<erase.down>", 160 213 "▲ foo 161 - │ /tmp/b  162 - └ should be /tmp/bar 163 - ", 164 - "<cursor.backward count=999><cursor.up count=4>", 214 + 215 + │ Search: /tmp/r█ 216 + │ should be /tmp/bar 217 + │ ● /tmp/root.zip 218 + │ ↑/↓ to select • Enter: confirm • Type: to search 219 + └", 220 + "<cursor.backward count=999><cursor.up count=7>", 165 221 "<cursor.down count=1>", 166 222 "<erase.down>", 167 223 "◆ foo 168 - │ /tmp/ba  169 - └ 170 - ", 171 - "<cursor.backward count=999><cursor.up count=4>", 172 - "<cursor.down count=2>", 173 - "<erase.line><cursor.left count=1>", 174 - "│ /tmp/bar ", 175 - "<cursor.down count=2>", 176 - "<cursor.backward count=999><cursor.up count=4>", 224 + 225 + │ Search: /tmp/█ 226 + │ ○ /tmp/bar 227 + │ ● /tmp/root.zip 228 + │ ↑/↓ to select • Enter: confirm • Type: to search 229 + └", 230 + "<cursor.backward count=999><cursor.up count=7>", 231 + "<cursor.down count=3>", 232 + "<erase.down>", 233 + "│ Search: /tmp/b█ 234 + │ ● /tmp/bar 235 + │ ↑/↓ to select • Enter: confirm • Type: to search 236 + └", 237 + "<cursor.backward count=999><cursor.up count=6>", 177 238 "<cursor.down count=1>", 178 239 "<erase.down>", 179 240 "◇ foo ··· 189 250 "<cursor.hide>", 190 251 "│ 191 252 ◆ foo 192 - │ /tmp/foo 193 - └ 194 - ", 195 - "<cursor.backward count=999><cursor.up count=4>", 196 - "<cursor.down count=2>", 197 - "<erase.line><cursor.left count=1>", 198 - "│ /tmp/b ", 199 - "<cursor.down count=2>", 200 - "<cursor.backward count=999><cursor.up count=4>", 253 + 254 + │ Search: /tmp/█ 255 + │ ● /tmp/bar 256 + │ ○ /tmp/root.zip 257 + │ ↑/↓ to select • Enter: confirm • Type: to search 258 + └", 259 + "<cursor.backward count=999><cursor.up count=7>", 260 + "<cursor.down count=3>", 261 + "<erase.down>", 262 + "│ Search: /tmp/r█ 263 + │ ● /tmp/root.zip 264 + │ ↑/↓ to select • Enter: confirm • Type: to search 265 + └", 266 + "<cursor.backward count=999><cursor.up count=6>", 201 267 "<cursor.down count=1>", 202 268 "<erase.down>", 203 269 "▲ foo 204 - │ /tmp/b  205 - └ should be /tmp/bar 206 - ", 207 - "<cursor.backward count=999><cursor.up count=4>", 270 + 271 + │ Search: /tmp/r█ 272 + │ should be /tmp/bar 273 + │ ● /tmp/root.zip 274 + │ ↑/↓ to select • Enter: confirm • Type: to search 275 + └", 276 + "<cursor.backward count=999><cursor.up count=7>", 208 277 "<cursor.down count=1>", 209 278 "<erase.down>", 210 279 "◆ foo 211 - │ /tmp/ba  212 - └ 213 - ", 214 - "<cursor.backward count=999><cursor.up count=4>", 215 - "<cursor.down count=2>", 216 - "<erase.line><cursor.left count=1>", 217 - "│ /tmp/bar ", 218 - "<cursor.down count=2>", 219 - "<cursor.backward count=999><cursor.up count=4>", 280 + 281 + │ Search: /tmp/█ 282 + │ ○ /tmp/bar 283 + │ ● /tmp/root.zip 284 + │ ↑/↓ to select • Enter: confirm • Type: to search 285 + └", 286 + "<cursor.backward count=999><cursor.up count=7>", 287 + "<cursor.down count=3>", 288 + "<erase.down>", 289 + "│ Search: /tmp/b█ 290 + │ ● /tmp/bar 291 + │ ↑/↓ to select • Enter: confirm • Type: to search 292 + └", 293 + "<cursor.backward count=999><cursor.up count=6>", 220 294 "<cursor.down count=1>", 221 295 "<erase.down>", 222 296 "◇ foo ··· 232 306 "<cursor.hide>", 233 307 "│ 234 308 ◆ foo 235 - │ /tmp/foo 236 - └ 237 - ", 238 - "<cursor.backward count=999><cursor.up count=4>", 309 + 310 + │ Search: /tmp/█ 311 + │ ● /tmp/bar 312 + │ ○ /tmp/root.zip 313 + │ ↑/↓ to select • Enter: confirm • Type: to search 314 + └", 315 + "<cursor.backward count=999><cursor.up count=7>", 239 316 "<cursor.down count=1>", 240 317 "<erase.down>", 241 318 "■ foo 242 - │ /tmp/ 243 - │", 319 + │ /tmp/", 244 320 " 245 321 ", 246 322 "<cursor.show>", 247 323 ] 248 324 `; 249 325 250 - exports[`text (isCI = true) > initialValue sets the value 1`] = ` 326 + exports[`text (isCI = true) > cannot submit unknown value 1`] = ` 251 327 [ 252 328 "<cursor.hide>", 253 329 "│ 254 330 ◆ foo 255 - │ /tmp/bar  256 - └ 257 - ", 258 - "<cursor.backward count=999><cursor.up count=4>", 331 + 332 + │ Search: /tmp/█ 333 + │ ● /tmp/bar 334 + │ ○ /tmp/root.zip 335 + │ ↑/↓ to select • Enter: confirm • Type: to search 336 + └", 337 + "<cursor.backward count=999><cursor.up count=7>", 338 + "<cursor.down count=3>", 339 + "<erase.down>", 340 + "│ Search: /tmp/_█ 341 + │ No matches found 342 + │ ↑/↓ to select • Enter: confirm • Type: to search 343 + └", 344 + "<cursor.backward count=999><cursor.up count=6>", 345 + "<cursor.down count=1>", 346 + "<erase.down>", 347 + "▲ foo 348 + 349 + │ Search: /tmp/_█ 350 + │ No matches found 351 + │ Please select a path 352 + │ ↑/↓ to select • Enter: confirm • Type: to search 353 + └", 354 + "<cursor.backward count=999><cursor.up count=7>", 355 + "<cursor.down count=1>", 356 + "<erase.down>", 357 + "◆ foo 358 + 359 + │ Search: /tmp/█ 360 + │ ● /tmp/bar 361 + │ ○ /tmp/root.zip 362 + │ ↑/↓ to select • Enter: confirm • Type: to search 363 + └", 364 + "<cursor.backward count=999><cursor.up count=7>", 365 + "<cursor.down count=3>", 366 + "<erase.down>", 367 + "│ Search: /tmp/b█ 368 + │ ● /tmp/bar 369 + │ ↑/↓ to select • Enter: confirm • Type: to search 370 + └", 371 + "<cursor.backward count=999><cursor.up count=6>", 259 372 "<cursor.down count=1>", 260 373 "<erase.down>", 261 374 "◇ foo ··· 266 379 ] 267 380 `; 268 381 269 - exports[`text (isCI = true) > renders and apply (<tab>) suggestion 1`] = ` 382 + exports[`text (isCI = true) > initialValue sets the value 1`] = ` 270 383 [ 271 384 "<cursor.hide>", 272 385 "│ 273 386 ◆ foo 274 - │ /tmp/foo 275 - └ 276 - ", 277 - "<cursor.backward count=999><cursor.up count=4>", 278 - "<cursor.down count=2>", 279 - "<erase.line><cursor.left count=1>", 280 - "│ /tmp/foo/bar.txt", 281 - "<cursor.down count=2>", 282 - "<cursor.backward count=999><cursor.up count=4>", 387 + 388 + │ Search: /tmp/bar█ 389 + │ ● /tmp/bar 390 + │ ↑/↓ to select • Enter: confirm • Type: to search 391 + └", 392 + "<cursor.backward count=999><cursor.up count=6>", 283 393 "<cursor.down count=1>", 284 394 "<erase.down>", 285 395 "◇ foo 286 - │ /tmp/foo", 396 + │ /tmp/bar", 287 397 " 288 398 ", 289 399 "<cursor.show>", ··· 295 405 "<cursor.hide>", 296 406 "│ 297 407 ◆ foo 298 - │ /tmp/foo 299 - └ 300 - ", 301 - "<cursor.backward count=999><cursor.up count=4>", 302 - "<cursor.down count=2>", 303 - "<erase.line><cursor.left count=1>", 304 - "│ /tmp/x ", 305 - "<cursor.down count=2>", 306 - "<cursor.backward count=999><cursor.up count=4>", 307 - "<cursor.down count=2>", 408 + 409 + │ Search: /tmp/█ 410 + │ ● /tmp/bar 411 + │ ○ /tmp/root.zip 412 + │ ↑/↓ to select • Enter: confirm • Type: to search 413 + └", 414 + "<cursor.backward count=999><cursor.up count=7>", 415 + "<cursor.down count=3>", 416 + "<erase.down>", 417 + "│ Search: /tmp/x█ 418 + │ No matches found 419 + │ ↑/↓ to select • Enter: confirm • Type: to search 420 + └", 421 + "<cursor.backward count=999><cursor.up count=6>", 422 + "<cursor.down count=3>", 308 423 "<erase.line><cursor.left count=1>", 309 - "│ /tmp/xy ", 310 - "<cursor.down count=2>", 311 - "<cursor.backward count=999><cursor.up count=4>", 424 + "│ Search: /tmp/xy█", 425 + "<cursor.down count=3>", 426 + "<cursor.backward count=999><cursor.up count=6>", 312 427 "<cursor.down count=1>", 313 428 "<erase.down>", 314 429 "■ foo 315 - │ /tmp/xy 316 - │", 430 + │ /tmp/xy", 317 431 " 318 432 ", 319 433 "<cursor.show>", ··· 325 439 "<cursor.hide>", 326 440 "│ 327 441 ◆ foo 328 - │ /tmp/foo 329 - └ 330 - ", 331 - "<cursor.backward count=999><cursor.up count=4>", 442 + 443 + │ Search: /tmp/█ 444 + │ ● /tmp/bar 445 + │ ○ /tmp/root.zip 446 + │ ↑/↓ to select • Enter: confirm • Type: to search 447 + └", 448 + "<cursor.backward count=999><cursor.up count=7>", 332 449 "<cursor.down count=1>", 333 450 "<erase.down>", 334 451 "◇ foo 335 - │ /tmp/", 452 + │ /tmp/bar", 336 453 " 337 454 ", 338 455 "<cursor.show>", ··· 344 461 "<cursor.hide>", 345 462 "│ 346 463 ◆ foo 347 - │ /tmp/foo 348 - └ 349 - ", 350 - "<cursor.backward count=999><cursor.up count=4>", 351 - "<cursor.down count=2>", 352 - "<erase.line><cursor.left count=1>", 353 - "│ /tmp/x ", 354 - "<cursor.down count=2>", 355 - "<cursor.backward count=999><cursor.up count=4>", 356 - "<cursor.down count=2>", 464 + 465 + │ Search: /tmp/█ 466 + │ ● /tmp/bar 467 + │ ○ /tmp/root.zip 468 + │ ↑/↓ to select • Enter: confirm • Type: to search 469 + └", 470 + "<cursor.backward count=999><cursor.up count=7>", 471 + "<cursor.down count=3>", 472 + "<erase.down>", 473 + "│ Search: /tmp/b█ 474 + │ ● /tmp/bar 475 + │ ↑/↓ to select • Enter: confirm • Type: to search 476 + └", 477 + "<cursor.backward count=999><cursor.up count=6>", 478 + "<cursor.down count=3>", 357 479 "<erase.line><cursor.left count=1>", 358 - "│ /tmp/xy ", 359 - "<cursor.down count=2>", 360 - "<cursor.backward count=999><cursor.up count=4>", 480 + "│ Search: /tmp/ba█", 481 + "<cursor.down count=3>", 482 + "<cursor.backward count=999><cursor.up count=6>", 361 483 "<cursor.down count=1>", 362 484 "<erase.down>", 363 485 "◇ foo 364 - │ /tmp/xy", 486 + │ /tmp/bar", 365 487 " 366 488 ", 367 489 "<cursor.show>", ··· 373 495 "<cursor.hide>", 374 496 "│ 375 497 ◆ foo 376 - │ /tmp/foo 377 - └ 378 - ", 379 - "<cursor.backward count=999><cursor.up count=4>", 380 - "<cursor.down count=2>", 381 - "<erase.line><cursor.left count=1>", 382 - "│ /tmp/b ", 383 - "<cursor.down count=2>", 384 - "<cursor.backward count=999><cursor.up count=4>", 498 + 499 + │ Search: /tmp/█ 500 + │ ● /tmp/bar 501 + │ ○ /tmp/root.zip 502 + │ ↑/↓ to select • Enter: confirm • Type: to search 503 + └", 504 + "<cursor.backward count=999><cursor.up count=7>", 505 + "<cursor.down count=3>", 506 + "<erase.down>", 507 + "│ Search: /tmp/r█ 508 + │ ● /tmp/root.zip 509 + │ ↑/↓ to select • Enter: confirm • Type: to search 510 + └", 511 + "<cursor.backward count=999><cursor.up count=6>", 385 512 "<cursor.down count=1>", 386 513 "<erase.down>", 387 514 "▲ foo 388 - │ /tmp/b  389 - └ should be /tmp/bar 390 - ", 391 - "<cursor.backward count=999><cursor.up count=4>", 515 + 516 + │ Search: /tmp/r█ 517 + │ should be /tmp/bar 518 + │ ● /tmp/root.zip 519 + │ ↑/↓ to select • Enter: confirm • Type: to search 520 + └", 521 + "<cursor.backward count=999><cursor.up count=7>", 392 522 "<cursor.down count=1>", 393 523 "<erase.down>", 394 524 "◆ foo 395 - │ /tmp/ba  396 - └ 397 - ", 398 - "<cursor.backward count=999><cursor.up count=4>", 399 - "<cursor.down count=2>", 400 - "<erase.line><cursor.left count=1>", 401 - "│ /tmp/bar ", 402 - "<cursor.down count=2>", 403 - "<cursor.backward count=999><cursor.up count=4>", 525 + 526 + │ Search: /tmp/█ 527 + │ ○ /tmp/bar 528 + │ ● /tmp/root.zip 529 + │ ↑/↓ to select • Enter: confirm • Type: to search 530 + └", 531 + "<cursor.backward count=999><cursor.up count=7>", 532 + "<cursor.down count=3>", 533 + "<erase.down>", 534 + "│ Search: /tmp/b█ 535 + │ ● /tmp/bar 536 + │ ↑/↓ to select • Enter: confirm • Type: to search 537 + └", 538 + "<cursor.backward count=999><cursor.up count=6>", 404 539 "<cursor.down count=1>", 405 540 "<erase.down>", 406 541 "◇ foo ··· 416 551 "<cursor.hide>", 417 552 "│ 418 553 ◆ foo 419 - │ /tmp/foo 420 - └ 421 - ", 422 - "<cursor.backward count=999><cursor.up count=4>", 423 - "<cursor.down count=2>", 424 - "<erase.line><cursor.left count=1>", 425 - "│ /tmp/b ", 426 - "<cursor.down count=2>", 427 - "<cursor.backward count=999><cursor.up count=4>", 554 + 555 + │ Search: /tmp/█ 556 + │ ● /tmp/bar 557 + │ ○ /tmp/root.zip 558 + │ ↑/↓ to select • Enter: confirm • Type: to search 559 + └", 560 + "<cursor.backward count=999><cursor.up count=7>", 561 + "<cursor.down count=3>", 562 + "<erase.down>", 563 + "│ Search: /tmp/r█ 564 + │ ● /tmp/root.zip 565 + │ ↑/↓ to select • Enter: confirm • Type: to search 566 + └", 567 + "<cursor.backward count=999><cursor.up count=6>", 428 568 "<cursor.down count=1>", 429 569 "<erase.down>", 430 570 "▲ foo 431 - │ /tmp/b  432 - └ should be /tmp/bar 433 - ", 434 - "<cursor.backward count=999><cursor.up count=4>", 571 + 572 + │ Search: /tmp/r█ 573 + │ should be /tmp/bar 574 + │ ● /tmp/root.zip 575 + │ ↑/↓ to select • Enter: confirm • Type: to search 576 + └", 577 + "<cursor.backward count=999><cursor.up count=7>", 435 578 "<cursor.down count=1>", 436 579 "<erase.down>", 437 580 "◆ foo 438 - │ /tmp/ba  439 - └ 440 - ", 441 - "<cursor.backward count=999><cursor.up count=4>", 442 - "<cursor.down count=2>", 443 - "<erase.line><cursor.left count=1>", 444 - "│ /tmp/bar ", 445 - "<cursor.down count=2>", 446 - "<cursor.backward count=999><cursor.up count=4>", 581 + 582 + │ Search: /tmp/█ 583 + │ ○ /tmp/bar 584 + │ ● /tmp/root.zip 585 + │ ↑/↓ to select • Enter: confirm • Type: to search 586 + └", 587 + "<cursor.backward count=999><cursor.up count=7>", 588 + "<cursor.down count=3>", 589 + "<erase.down>", 590 + "│ Search: /tmp/b█ 591 + │ ● /tmp/bar 592 + │ ↑/↓ to select • Enter: confirm • Type: to search 593 + └", 594 + "<cursor.backward count=999><cursor.up count=6>", 447 595 "<cursor.down count=1>", 448 596 "<erase.down>", 449 597 "◇ foo
-433
packages/prompts/test/__snapshots__/suggestion.test.ts.snap
··· 1 - // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html 2 - 3 - exports[`text (isCI = false) > can cancel 1`] = ` 4 - [ 5 - "<cursor.hide>", 6 - "│ 7 - ◆ foo 8 - │   9 - └ 10 - ", 11 - "<cursor.backward count=999><cursor.up count=4>", 12 - "<cursor.down count=1>", 13 - "<erase.down>", 14 - "■ foo 15 - │", 16 - " 17 - ", 18 - "<cursor.show>", 19 - ] 20 - `; 21 - 22 - exports[`text (isCI = false) > initialValue sets the value 1`] = ` 23 - [ 24 - "<cursor.hide>", 25 - "│ 26 - ◆ foo 27 - │ bar  28 - └ 29 - ", 30 - "<cursor.backward count=999><cursor.up count=4>", 31 - "<cursor.down count=1>", 32 - "<erase.down>", 33 - "◇ foo 34 - │ bar", 35 - " 36 - ", 37 - "<cursor.show>", 38 - ] 39 - `; 40 - 41 - exports[`text (isCI = false) > renders and apply (<tab>) suggestion 1`] = ` 42 - [ 43 - "<cursor.hide>", 44 - "│ 45 - ◆ foo 46 - │ bar 47 - └ 48 - ", 49 - "<cursor.backward count=999><cursor.up count=4>", 50 - "<cursor.down count=2>", 51 - "<erase.line><cursor.left count=1>", 52 - "│ bar ", 53 - "<cursor.down count=2>", 54 - "<cursor.backward count=999><cursor.up count=4>", 55 - "<cursor.down count=1>", 56 - "<erase.down>", 57 - "◇ foo 58 - │ bar", 59 - " 60 - ", 61 - "<cursor.show>", 62 - ] 63 - `; 64 - 65 - exports[`text (isCI = false) > renders cancelled value if one set 1`] = ` 66 - [ 67 - "<cursor.hide>", 68 - "│ 69 - ◆ foo 70 - │ xyz 71 - └ 72 - ", 73 - "<cursor.backward count=999><cursor.up count=4>", 74 - "<cursor.down count=2>", 75 - "<erase.line><cursor.left count=1>", 76 - "│ xyz", 77 - "<cursor.down count=2>", 78 - "<cursor.backward count=999><cursor.up count=4>", 79 - "<cursor.down count=2>", 80 - "<erase.line><cursor.left count=1>", 81 - "│ xyz", 82 - "<cursor.down count=2>", 83 - "<cursor.backward count=999><cursor.up count=4>", 84 - "<cursor.down count=1>", 85 - "<erase.down>", 86 - "■ foo 87 - │ xy 88 - │", 89 - " 90 - ", 91 - "<cursor.show>", 92 - ] 93 - `; 94 - 95 - exports[`text (isCI = false) > renders message 1`] = ` 96 - [ 97 - "<cursor.hide>", 98 - "│ 99 - ◆ foo 100 - │   101 - └ 102 - ", 103 - "<cursor.backward count=999><cursor.up count=4>", 104 - "<cursor.down count=1>", 105 - "<erase.down>", 106 - "◇ foo 107 - │", 108 - " 109 - ", 110 - "<cursor.show>", 111 - ] 112 - `; 113 - 114 - exports[`text (isCI = false) > renders submitted value 1`] = ` 115 - [ 116 - "<cursor.hide>", 117 - "│ 118 - ◆ foo 119 - │ xyz 120 - └ 121 - ", 122 - "<cursor.backward count=999><cursor.up count=4>", 123 - "<cursor.down count=2>", 124 - "<erase.line><cursor.left count=1>", 125 - "│ xyz", 126 - "<cursor.down count=2>", 127 - "<cursor.backward count=999><cursor.up count=4>", 128 - "<cursor.down count=2>", 129 - "<erase.line><cursor.left count=1>", 130 - "│ xyz", 131 - "<cursor.down count=2>", 132 - "<cursor.backward count=999><cursor.up count=4>", 133 - "<cursor.down count=1>", 134 - "<erase.down>", 135 - "◇ foo 136 - │ xy", 137 - " 138 - ", 139 - "<cursor.show>", 140 - ] 141 - `; 142 - 143 - exports[`text (isCI = false) > validation errors render and clear (using Error) 1`] = ` 144 - [ 145 - "<cursor.hide>", 146 - "│ 147 - ◆ foo 148 - │ xyz 149 - └ 150 - ", 151 - "<cursor.backward count=999><cursor.up count=4>", 152 - "<cursor.down count=2>", 153 - "<erase.line><cursor.left count=1>", 154 - "│ xyz", 155 - "<cursor.down count=2>", 156 - "<cursor.backward count=999><cursor.up count=4>", 157 - "<cursor.down count=1>", 158 - "<erase.down>", 159 - "▲ foo 160 - │ xyz 161 - └ should be xy 162 - ", 163 - "<cursor.backward count=999><cursor.up count=4>", 164 - "<cursor.down count=1>", 165 - "<erase.down>", 166 - "◆ foo 167 - │ xyz 168 - └ 169 - ", 170 - "<cursor.backward count=999><cursor.up count=4>", 171 - "<cursor.down count=1>", 172 - "<erase.down>", 173 - "◇ foo 174 - │ xy", 175 - " 176 - ", 177 - "<cursor.show>", 178 - ] 179 - `; 180 - 181 - exports[`text (isCI = false) > validation errors render and clear 1`] = ` 182 - [ 183 - "<cursor.hide>", 184 - "│ 185 - ◆ foo 186 - │ xyz 187 - └ 188 - ", 189 - "<cursor.backward count=999><cursor.up count=4>", 190 - "<cursor.down count=2>", 191 - "<erase.line><cursor.left count=1>", 192 - "│ xyz", 193 - "<cursor.down count=2>", 194 - "<cursor.backward count=999><cursor.up count=4>", 195 - "<cursor.down count=1>", 196 - "<erase.down>", 197 - "▲ foo 198 - │ xyz 199 - └ should be xy 200 - ", 201 - "<cursor.backward count=999><cursor.up count=4>", 202 - "<cursor.down count=1>", 203 - "<erase.down>", 204 - "◆ foo 205 - │ xyz 206 - └ 207 - ", 208 - "<cursor.backward count=999><cursor.up count=4>", 209 - "<cursor.down count=1>", 210 - "<erase.down>", 211 - "◇ foo 212 - │ xy", 213 - " 214 - ", 215 - "<cursor.show>", 216 - ] 217 - `; 218 - 219 - exports[`text (isCI = true) > can cancel 1`] = ` 220 - [ 221 - "<cursor.hide>", 222 - "│ 223 - ◆ foo 224 - │   225 - └ 226 - ", 227 - "<cursor.backward count=999><cursor.up count=4>", 228 - "<cursor.down count=1>", 229 - "<erase.down>", 230 - "■ foo 231 - │", 232 - " 233 - ", 234 - "<cursor.show>", 235 - ] 236 - `; 237 - 238 - exports[`text (isCI = true) > initialValue sets the value 1`] = ` 239 - [ 240 - "<cursor.hide>", 241 - "│ 242 - ◆ foo 243 - │ bar  244 - └ 245 - ", 246 - "<cursor.backward count=999><cursor.up count=4>", 247 - "<cursor.down count=1>", 248 - "<erase.down>", 249 - "◇ foo 250 - │ bar", 251 - " 252 - ", 253 - "<cursor.show>", 254 - ] 255 - `; 256 - 257 - exports[`text (isCI = true) > renders and apply (<tab>) suggestion 1`] = ` 258 - [ 259 - "<cursor.hide>", 260 - "│ 261 - ◆ foo 262 - │ bar 263 - └ 264 - ", 265 - "<cursor.backward count=999><cursor.up count=4>", 266 - "<cursor.down count=2>", 267 - "<erase.line><cursor.left count=1>", 268 - "│ bar ", 269 - "<cursor.down count=2>", 270 - "<cursor.backward count=999><cursor.up count=4>", 271 - "<cursor.down count=1>", 272 - "<erase.down>", 273 - "◇ foo 274 - │ bar", 275 - " 276 - ", 277 - "<cursor.show>", 278 - ] 279 - `; 280 - 281 - exports[`text (isCI = true) > renders cancelled value if one set 1`] = ` 282 - [ 283 - "<cursor.hide>", 284 - "│ 285 - ◆ foo 286 - │ xyz 287 - └ 288 - ", 289 - "<cursor.backward count=999><cursor.up count=4>", 290 - "<cursor.down count=2>", 291 - "<erase.line><cursor.left count=1>", 292 - "│ xyz", 293 - "<cursor.down count=2>", 294 - "<cursor.backward count=999><cursor.up count=4>", 295 - "<cursor.down count=2>", 296 - "<erase.line><cursor.left count=1>", 297 - "│ xyz", 298 - "<cursor.down count=2>", 299 - "<cursor.backward count=999><cursor.up count=4>", 300 - "<cursor.down count=1>", 301 - "<erase.down>", 302 - "■ foo 303 - │ xy 304 - │", 305 - " 306 - ", 307 - "<cursor.show>", 308 - ] 309 - `; 310 - 311 - exports[`text (isCI = true) > renders message 1`] = ` 312 - [ 313 - "<cursor.hide>", 314 - "│ 315 - ◆ foo 316 - │   317 - └ 318 - ", 319 - "<cursor.backward count=999><cursor.up count=4>", 320 - "<cursor.down count=1>", 321 - "<erase.down>", 322 - "◇ foo 323 - │", 324 - " 325 - ", 326 - "<cursor.show>", 327 - ] 328 - `; 329 - 330 - exports[`text (isCI = true) > renders submitted value 1`] = ` 331 - [ 332 - "<cursor.hide>", 333 - "│ 334 - ◆ foo 335 - │ xyz 336 - └ 337 - ", 338 - "<cursor.backward count=999><cursor.up count=4>", 339 - "<cursor.down count=2>", 340 - "<erase.line><cursor.left count=1>", 341 - "│ xyz", 342 - "<cursor.down count=2>", 343 - "<cursor.backward count=999><cursor.up count=4>", 344 - "<cursor.down count=2>", 345 - "<erase.line><cursor.left count=1>", 346 - "│ xyz", 347 - "<cursor.down count=2>", 348 - "<cursor.backward count=999><cursor.up count=4>", 349 - "<cursor.down count=1>", 350 - "<erase.down>", 351 - "◇ foo 352 - │ xy", 353 - " 354 - ", 355 - "<cursor.show>", 356 - ] 357 - `; 358 - 359 - exports[`text (isCI = true) > validation errors render and clear (using Error) 1`] = ` 360 - [ 361 - "<cursor.hide>", 362 - "│ 363 - ◆ foo 364 - │ xyz 365 - └ 366 - ", 367 - "<cursor.backward count=999><cursor.up count=4>", 368 - "<cursor.down count=2>", 369 - "<erase.line><cursor.left count=1>", 370 - "│ xyz", 371 - "<cursor.down count=2>", 372 - "<cursor.backward count=999><cursor.up count=4>", 373 - "<cursor.down count=1>", 374 - "<erase.down>", 375 - "▲ foo 376 - │ xyz 377 - └ should be xy 378 - ", 379 - "<cursor.backward count=999><cursor.up count=4>", 380 - "<cursor.down count=1>", 381 - "<erase.down>", 382 - "◆ foo 383 - │ xyz 384 - └ 385 - ", 386 - "<cursor.backward count=999><cursor.up count=4>", 387 - "<cursor.down count=1>", 388 - "<erase.down>", 389 - "◇ foo 390 - │ xy", 391 - " 392 - ", 393 - "<cursor.show>", 394 - ] 395 - `; 396 - 397 - exports[`text (isCI = true) > validation errors render and clear 1`] = ` 398 - [ 399 - "<cursor.hide>", 400 - "│ 401 - ◆ foo 402 - │ xyz 403 - └ 404 - ", 405 - "<cursor.backward count=999><cursor.up count=4>", 406 - "<cursor.down count=2>", 407 - "<erase.line><cursor.left count=1>", 408 - "│ xyz", 409 - "<cursor.down count=2>", 410 - "<cursor.backward count=999><cursor.up count=4>", 411 - "<cursor.down count=1>", 412 - "<erase.down>", 413 - "▲ foo 414 - │ xyz 415 - └ should be xy 416 - ", 417 - "<cursor.backward count=999><cursor.up count=4>", 418 - "<cursor.down count=1>", 419 - "<erase.down>", 420 - "◆ foo 421 - │ xyz 422 - └ 423 - ", 424 - "<cursor.backward count=999><cursor.up count=4>", 425 - "<cursor.down count=1>", 426 - "<erase.down>", 427 - "◇ foo 428 - │ xy", 429 - " 430 - ", 431 - "<cursor.show>", 432 - ] 433 - `;
+34 -26
packages/prompts/test/path.test.ts
··· 31 31 './hello/john.jpg': '4', 32 32 './hello/jeanne.png': '5', 33 33 './root.zip': '6', 34 + './bar': '7', 34 35 }, 35 36 '/tmp' 36 37 ); ··· 50 51 51 52 input.emit('keypress', '', { name: 'return' }); 52 53 53 - await result; 54 + const value = await result; 54 55 55 56 expect(output.buffer).toMatchSnapshot(); 57 + expect(value).toBe('/tmp/bar'); 56 58 }); 57 59 58 - test('renders and apply (<tab>) suggestion', async () => { 60 + test('can cancel', async () => { 59 61 const result = prompts.path({ 60 62 message: 'foo', 61 - root: '/tmp', 63 + root: '/tmp/', 62 64 input, 63 65 output, 64 66 }); 65 67 66 - input.emit('keypress', '\t', { name: 'tab' }); 67 - input.emit('keypress', '', { name: 'return' }); 68 + input.emit('keypress', 'escape', { name: 'escape' }); 68 69 69 70 const value = await result; 70 71 72 + expect(prompts.isCancel(value)).toBe(true); 71 73 expect(output.buffer).toMatchSnapshot(); 72 - 73 - expect(value).toBe('/tmp/foo'); 74 74 }); 75 75 76 - test('can cancel', async () => { 76 + test('renders cancelled value if one set', async () => { 77 77 const result = prompts.path({ 78 78 message: 'foo', 79 - root: '/tmp/', 80 79 input, 81 80 output, 81 + root: '/tmp/', 82 82 }); 83 83 84 - input.emit('keypress', 'escape', { name: 'escape' }); 84 + input.emit('keypress', 'x', { name: 'x' }); 85 + input.emit('keypress', 'y', { name: 'y' }); 86 + input.emit('keypress', '', { name: 'escape' }); 85 87 86 88 const value = await result; 87 89 ··· 89 91 expect(output.buffer).toMatchSnapshot(); 90 92 }); 91 93 92 - test('renders cancelled value if one set', async () => { 94 + test('renders submitted value', async () => { 93 95 const result = prompts.path({ 94 96 message: 'foo', 97 + root: '/tmp/', 95 98 input, 96 99 output, 97 - root: '/tmp/', 98 100 }); 99 101 100 - input.emit('keypress', 'x', { name: 'x' }); 101 - input.emit('keypress', 'y', { name: 'y' }); 102 - input.emit('keypress', '', { name: 'escape' }); 102 + input.emit('keypress', 'b', { name: 'b' }); 103 + input.emit('keypress', 'a', { name: 'a' }); 104 + input.emit('keypress', '', { name: 'return' }); 103 105 104 106 const value = await result; 105 107 106 - expect(prompts.isCancel(value)).toBe(true); 108 + expect(value).toBe('/tmp/bar'); 107 109 expect(output.buffer).toMatchSnapshot(); 108 110 }); 109 111 110 - test('renders submitted value', async () => { 112 + test('cannot submit unknown value', async () => { 111 113 const result = prompts.path({ 112 114 message: 'foo', 113 115 root: '/tmp/', ··· 115 117 output, 116 118 }); 117 119 118 - input.emit('keypress', 'x', { name: 'x' }); 119 - input.emit('keypress', 'y', { name: 'y' }); 120 + input.emit('keypress', '_', { name: '_' }); 121 + input.emit('keypress', '', { name: 'return' }); 122 + input.emit('keypress', '', { name: 'h', ctrl: true }); 123 + input.emit('keypress', 'b', { name: 'b' }); 120 124 input.emit('keypress', '', { name: 'return' }); 121 125 122 126 const value = await result; 123 127 124 - expect(value).toBe('/tmp/xy'); 128 + expect(value).toBe('/tmp/bar'); 125 129 expect(output.buffer).toMatchSnapshot(); 126 130 }); 127 131 ··· 151 155 output, 152 156 }); 153 157 154 - input.emit('keypress', 'b', { name: 'b' }); 158 + // to match `root.zip` 159 + input.emit('keypress', 'r', { name: 'r' }); 155 160 input.emit('keypress', '', { name: 'return' }); 156 - input.emit('keypress', 'a', { name: 'a' }); 157 - input.emit('keypress', 'r', { name: 'r' }); 161 + // delete what we had 162 + input.emit('keypress', '', { name: 'h', ctrl: true }); 163 + input.emit('keypress', 'b', { name: 'b' }); 158 164 input.emit('keypress', '', { name: 'return' }); 159 165 160 166 const value = await result; ··· 172 178 output, 173 179 }); 174 180 175 - input.emit('keypress', 'b', { name: 'b' }); 176 - input.emit('keypress', '', { name: 'return' }); 177 - input.emit('keypress', 'a', { name: 'a' }); 181 + // to match `root.zip` 178 182 input.emit('keypress', 'r', { name: 'r' }); 183 + input.emit('keypress', '', { name: 'return' }); 184 + // delete what we had 185 + input.emit('keypress', '', { name: 'h', ctrl: true }); 186 + input.emit('keypress', 'b', { name: 'b' }); 179 187 input.emit('keypress', '', { name: 'return' }); 180 188 181 189 const value = await result;
-169
packages/prompts/test/suggestion.test.ts
··· 1 - import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 2 - import * as prompts from '../src/index.js'; 3 - import { MockReadable, MockWritable } from './test-utils.js'; 4 - 5 - describe.each(['true', 'false'])('text (isCI = %s)', (isCI) => { 6 - let originalCI: string | undefined; 7 - let output: MockWritable; 8 - let input: MockReadable; 9 - 10 - beforeAll(() => { 11 - originalCI = process.env.CI; 12 - process.env.CI = isCI; 13 - }); 14 - 15 - afterAll(() => { 16 - process.env.CI = originalCI; 17 - }); 18 - 19 - beforeEach(() => { 20 - output = new MockWritable(); 21 - input = new MockReadable(); 22 - }); 23 - 24 - afterEach(() => { 25 - vi.restoreAllMocks(); 26 - }); 27 - 28 - test('renders message', async () => { 29 - const result = prompts.suggestion({ 30 - message: 'foo', 31 - input, 32 - output, 33 - suggest: () => [], 34 - }); 35 - 36 - input.emit('keypress', '', { name: 'return' }); 37 - 38 - await result; 39 - 40 - expect(output.buffer).toMatchSnapshot(); 41 - }); 42 - 43 - test('renders and apply (<tab>) suggestion', async () => { 44 - const result = prompts.suggestion({ 45 - message: 'foo', 46 - suggest: () => ['bar'], 47 - input, 48 - output, 49 - }); 50 - 51 - input.emit('keypress', '\t', { name: 'tab' }); 52 - input.emit('keypress', '', { name: 'return' }); 53 - 54 - const value = await result; 55 - 56 - expect(output.buffer).toMatchSnapshot(); 57 - 58 - expect(value).toBe('bar'); 59 - }); 60 - 61 - test('can cancel', async () => { 62 - const result = prompts.suggestion({ 63 - message: 'foo', 64 - suggest: () => [], 65 - input, 66 - output, 67 - }); 68 - 69 - input.emit('keypress', 'escape', { name: 'escape' }); 70 - 71 - const value = await result; 72 - 73 - expect(prompts.isCancel(value)).toBe(true); 74 - expect(output.buffer).toMatchSnapshot(); 75 - }); 76 - 77 - test('renders cancelled value if one set', async () => { 78 - const result = prompts.suggestion({ 79 - message: 'foo', 80 - input, 81 - output, 82 - suggest: () => ['xyz'], 83 - }); 84 - 85 - input.emit('keypress', 'x', { name: 'x' }); 86 - input.emit('keypress', 'y', { name: 'y' }); 87 - input.emit('keypress', '', { name: 'escape' }); 88 - 89 - const value = await result; 90 - 91 - expect(prompts.isCancel(value)).toBe(true); 92 - expect(output.buffer).toMatchSnapshot(); 93 - }); 94 - 95 - test('renders submitted value', async () => { 96 - const result = prompts.suggestion({ 97 - message: 'foo', 98 - suggest: () => ['xyz'], 99 - input, 100 - output, 101 - }); 102 - 103 - input.emit('keypress', 'x', { name: 'x' }); 104 - input.emit('keypress', 'y', { name: 'y' }); 105 - input.emit('keypress', '', { name: 'return' }); 106 - 107 - const value = await result; 108 - 109 - expect(value).toBe('xy'); 110 - expect(output.buffer).toMatchSnapshot(); 111 - }); 112 - 113 - test('initialValue sets the value', async () => { 114 - const result = prompts.suggestion({ 115 - message: 'foo', 116 - initialValue: 'bar', 117 - suggest: () => [], 118 - input, 119 - output, 120 - }); 121 - 122 - input.emit('keypress', '', { name: 'return' }); 123 - 124 - const value = await result; 125 - 126 - expect(value).toBe('bar'); 127 - expect(output.buffer).toMatchSnapshot(); 128 - }); 129 - 130 - test('validation errors render and clear', async () => { 131 - const result = prompts.suggestion({ 132 - message: 'foo', 133 - suggest: () => ['xyz'], 134 - validate: (val) => (val !== 'xy' ? 'should be xy' : undefined), 135 - input, 136 - output, 137 - }); 138 - 139 - input.emit('keypress', 'x', { name: 'x' }); 140 - input.emit('keypress', '', { name: 'return' }); 141 - input.emit('keypress', 'y', { name: 'y' }); 142 - input.emit('keypress', '', { name: 'return' }); 143 - 144 - const value = await result; 145 - 146 - expect(value).toBe('xy'); 147 - expect(output.buffer).toMatchSnapshot(); 148 - }); 149 - 150 - test('validation errors render and clear (using Error)', async () => { 151 - const result = prompts.suggestion({ 152 - message: 'foo', 153 - suggest: () => ['xyz'], 154 - validate: (val) => (val !== 'xy' ? new Error('should be xy') : undefined), 155 - input, 156 - output, 157 - }); 158 - 159 - input.emit('keypress', 'x', { name: 'x' }); 160 - input.emit('keypress', '', { name: 'return' }); 161 - input.emit('keypress', 'y', { name: 'y' }); 162 - input.emit('keypress', '', { name: 'return' }); 163 - 164 - const value = await result; 165 - 166 - expect(value).toBe('xy'); 167 - expect(output.buffer).toMatchSnapshot(); 168 - }); 169 - });