[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.

Merge branch 'main' into good-spinner

James Garbutt (Feb 24, 2026, 9:56 AM UTC) 4d48890a f95d408e

+1621 -408
+240
.changeset/README.md
··· 6 6 7 7 We have a quick list of common questions to get you started engaging with this project in 8 8 [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md) 9 + 10 + You will be responsible for writing a **changeset** for your Astro PR. This is a Markdown file that states: 11 + 12 + - which repository has changed 13 + - the kind of change according to [Astro's semantic versioning](https://docs.astro.build/en/upgrade-astro/#semantic-versioning) 14 + - a message describing the change that will be publicly displayed on the repo's CHANGELOG 15 + 16 + ```md title=".changeset/my-super-cool-changeset.md" wrap 17 + --- 18 + "astro": patch 19 + --- 20 + 21 + Fixes unexpected `undefined` value when calling an action from the client without a return value 22 + ``` 23 + 24 + You can generate a changeset using the `pnpm changeset` command, which will prompt you for the necessary information and create a randomly-named file in the `.changeset` folder. You can then edit the generated file to add more detail, or write your changeset from scratch. 25 + 26 + ## Format 27 + 28 + Begin your changeset message with **a present tense verb** that completes a sentence in the style of: "This PR . . ." or "This contribution ..." 29 + 30 + - Adds 31 + - Removes 32 + - Fixes 33 + - Updates 34 + - Refactors 35 + - Improves 36 + - Deprecates 37 + 38 + Finish the introductory sentence with a message focusing on what has changed **about the codebase** (not what your feature itself does) that is meaningful to a **user** of Astro. It is usually more helpful to describe the change **as someone building an Astro site will experience it**, instead of describing **how you fixed it** or **what the code in the PR does**: 39 + 40 + ```markdown title="changeset.md" del={2} ins={5} 41 + // What the code now does 42 + Logs helpful errors if content is invalid 43 + 44 + // The nature of the change to the Astro code base 45 + Adds logging for content collections configuration errors. 46 + ``` 47 + 48 + You may then include additional paragraphs if necessary to describe the change in more detail. This may not be needed for a small bug fix, but is often helpful if the reader must make a change to their own code, or needs to understand how the change might affect them. 49 + 50 + The level of detail of a changeset depends on the type of change (e.g. `patch` vs `minor`, breaking or not, only affects integration authors etc.) You may also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your longer content into sections. 51 + 52 + :::tip[Don't hide the good stuff in the changeset!] 53 + CHANGELOGS are often read only once, when someone is updating to the latest version of a package. Documentation is a constant reference that will be revisited and consulted frequently. 54 + 55 + Make sure that the helpful explanations and examples for your fellow developers in both your changeset and your PR description are captured in the actual feature documentation, too! 56 + ::: 57 + 58 + ### Patch updates 59 + 60 + These updates are often fixes, refactors or other small improvements. They are typically not user-facing, and do not require someone to update their own project code. 61 + 62 + Verbs like "fixes" and "refactors" are helpful to let readers know this is an internal or implementation change that they do not need to worry about. At the same time, these messages are helpful to someone who is interested in keeping up with small changes to the codebase. 63 + 64 + Often one line is enough to describe your change meaningfully to an Astro user: 65 + 66 + ```md title="my-patch-changeset.md" wrap 67 + --- 68 + "astro": patch 69 + --- 70 + 71 + Fixes a bug where the toolbar audit would incorrectly flag images as above the fold 72 + ``` 73 + 74 + ```md title="my-patch-changeset.md" wrap 75 + --- 76 + "astro": patch 77 + --- 78 + 79 + Refactors internal handling of styles and scripts for content collections to improve build performance 80 + ``` 81 + 82 + ```md title="my-patch-changeset.md" wrap 83 + --- 84 + "astro": patch 85 + --- 86 + 87 + Updates the `HTMLAttributes` type exported from `astro` to allow data attributes 88 + ``` 89 + 90 + These do not need to be full sentences and do not need end punctuation unless you write multiple sentences. 91 + 92 + :::tip[Help your reader figure out if this is change important to them] 93 + Even though these are small changes described by very short sentences, they still need to communicate a lot! 94 + 95 + Check that you have clearly stated not just **what has changed** but also **who needs to know**. If your reader can identify that a change is important to them, they can always seek out further information if they need to know more. 96 + ::: 97 + 98 + #### Tips and Examples 99 + 100 + For a great changeset message: 101 + 102 + Include the specific API changed (using inline code highlighting as appropriate) when your change might not be easy to identify so that your reader can easily tell whether it's something they are using and need to care about: 103 + 104 + > Improves automatic fallbacks generation 105 + > 106 + > vs 107 + > 108 + > ✅ Improves automatic `fallbacks` generation **for the experimental Fonts API** 109 + 110 + When the specific API change is not user-facing (e.g. a type not publicly exposed) and/or your reader will not recognize it by name, describe the use case or end result that will be meaningful to the reader instead: 111 + 112 + > Adds `| (string & {})` for better autocomplete of `App.SessionData` 113 + > 114 + > vs 115 + > 116 + > ✅ Improves autocompletion for session keys 117 + 118 + ### New features 119 + 120 + Begin your changeset with "Adds" to alert readers that there is something new and mention the names of any new items (options, functions) that have been added directly in the first sentence: 121 + 122 + ```md title="my-minor-changeset.md" wrap 123 + --- 124 + "astro": minor 125 + --- 126 + 127 + Adds a new `flamethrow` view transitions animation 128 + 129 + <!-- --> 130 + ``` 131 + 132 + Additionally, describe what people are now able to do because of these additions that they could not before. 133 + 134 + The changeset is an opportunity to call people's attention to new things they might wish to try in their Astro project, and may include a code example showing basic usage of the new feature: 135 + 136 + ````md wrap title="my-minor-changeset.md" 137 + --- 138 + "astro": minor 139 + --- 140 + 141 + Adds a new, optional property `timeout` for the `client:idle` directive 142 + 143 + This value allows you to specify a maximum time to wait, in milliseconds, before hydrating a UI framework component, even if the page is not yet done with its initial load. 144 + 145 + This means you can delay hydration for lower-priority UI elements with more control to ensure your element is interactive within a specified time frame. 146 + 147 + ``` 148 + <Button client:idle={{ timeout: 500 }} /> 149 + ``` 150 + ```` 151 + 152 + You can also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your longer content into sections. 153 + 154 + ### Breaking changes 155 + 156 + Changesets focus on what has changed, and **must include any breaking changes**, including changes to default behavior. Most users will have several default settings configured (often by not setting any value themselves), so changes to defaults can have a significant impact on someone's project! 157 + 158 + Verbs like "removes", "changes", and "deprecates" call attention to something that might require attention. Unlike a new feature someone can choose not to use, changing default or expected behavior cannot be ignored. 159 + 160 + You can also use [`<h4>` - `<h6>` Markdown headings](#using-markdown-section-headings) to break your content into sections, for example, to add headings like, "What should I do?" or "Migrating from the previous version." 161 + 162 + ```md title="my-major-changeset.md" wrap 163 + --- 164 + "astro": major 165 + --- 166 + 167 + Removes support for returning simple objects from endpoints. You must now return a `Response` instead. 168 + ``` 169 + 170 + Changeset messages for breaking changes must also provide clear guidance for updating. Diff code samples are encouraged when appropriate: 171 + 172 + ````md title="my-major-changeset.md" wrap 173 + --- 174 + "astro": major 175 + --- 176 + 177 + Removes support for Shiki custom language's `path` property. The language JSON file must now be imported and passed to the option instead. 178 + 179 + ```diff 180 + // astro.config.js 181 + + import customLang from './custom.tmLanguage.json' 182 + 183 + export default defineConfig({ 184 + markdown: { 185 + shikiConfig: { 186 + langs: [ 187 + - { path: './custom.tmLanguage.json' }, 188 + + customLang, 189 + ], 190 + }, 191 + }, 192 + }) 193 + ``` 194 + ```` 195 + 196 + If your contribution changes a default value, then it is helpful to describe how to revert back to the previous behavior. Readers may also appreciate being informed that they can remove configuration that is no longer needed. 197 + 198 + ````md title="my-major-changeset.md" wrap 199 + --- 200 + "astro": major 201 + --- 202 + 203 + Changes the default value of `security.checkOrigin` to true, which now enables Cross-Site Request Forgery (CSRF) protection by default for pages rendered on demand. 204 + 205 + If you had previously configured `security.checkOrigin: true`, you no longer need this set in your Astro config. This is now the default and it is safe to remove. 206 + 207 + To revert to the previous default behavior and opt out of automatically checking that the “origin” header matches the URL sent by each request, you must now explicitly set `security.checkOrigin: false`: 208 + 209 + ```diff 210 + export default defineConfig({ 211 + + security: { 212 + + checkOrigin: false 213 + + } 214 + }) 215 + ``` 216 + ```` 217 + 218 + ## Using Markdown section headings 219 + 220 + For longer descriptions that you want to split into sections, always start at the `<h4>` level, using `####` where you would normally use `##` in a regular Markdown post. This ensures readability when your message is incorporated into the final CHANGELOG: 221 + 222 + ```md wrap title="my-long-changeset-with-sections.md" 223 + --- 224 + "astro": minor 225 + --- 226 + 227 + Adds a new Sessions API to store user state between requests for on-demand rendered pages. 228 + 229 + #### Configuring session storage 230 + 231 + <!-- ... --> 232 + 233 + #### Using sessions 234 + 235 + <!-- ... --> 236 + 237 + ##### In `.astro` pages 238 + 239 + <!-- ... --> 240 + 241 + ##### In API endpoints 242 + 243 + <!-- ... --> 244 + 245 + #### Upgrading from the experimental API 246 + 247 + <!-- ... --> 248 + ```
+5
.changeset/cool-parrots-throw.md
··· 1 + --- 2 + "@clack/prompts": patch 3 + --- 4 + 5 + destruct `limitOption` param for better code readability, tweak types definitions
+6
.changeset/huge-items-throw.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + "@clack/core": minor 4 + --- 5 + 6 + Replaces `picocolors` with Node.js built-in `styleText`.
+3
.gitignore
··· 124 124 # Stores VSCode versions used for testing VSCode extensions 125 125 .vscode-test 126 126 127 + # JetBrains IDEA 128 + .idea 129 + 127 130 # yarn v2 128 131 .yarn/cache 129 132 .yarn/unplugged
+7
packages/core/CHANGELOG.md
··· 1 1 # @clack/core 2 2 3 + ## 1.0.1 4 + 5 + ### Patch Changes 6 + 7 + - 6404dc1: Disallows selection of `disabled` options in autocomplete. 8 + - 2533180: Updates the documentation to mention `userInputWithCursor` when using the `TextPrompt` primitive. 9 + 3 10 ## 1.0.0 4 11 5 12 ### Major Changes
+1 -1
packages/core/README.md
··· 11 11 12 12 const p = new TextPrompt({ 13 13 render() { 14 - return `What's your name?\n${this.valueWithCursor}`; 14 + return `What's your name?\n${this.userInputWithCursor}`; 15 15 }, 16 16 }); 17 17
+1 -2
packages/core/package.json
··· 1 1 { 2 2 "name": "@clack/core", 3 - "version": "1.0.0", 3 + "version": "1.0.1", 4 4 "type": "module", 5 5 "main": "./dist/index.mjs", 6 6 "module": "./dist/index.mjs", ··· 53 53 "test": "vitest run" 54 54 }, 55 55 "dependencies": { 56 - "picocolors": "^1.0.0", 57 56 "sisteransi": "^1.0.5" 58 57 }, 59 58 "devDependencies": {
+14 -9
packages/core/src/prompts/autocomplete.ts
··· 1 1 import type { Key } from 'node:readline'; 2 - import color from 'picocolors'; 2 + import { styleText } from 'node:util'; 3 + import { findCursor } from '../utils/cursor.js'; 3 4 import Prompt, { type PromptOptions } from './prompt.js'; 4 5 5 6 interface OptionLike { 6 7 value: unknown; 7 8 label?: string; 9 + disabled?: boolean; 8 10 } 9 11 10 12 type FilterFunction<T extends OptionLike> = (search: string, opt: T) => boolean; ··· 71 73 72 74 get userInputWithCursor() { 73 75 if (!this.userInput) { 74 - return color.inverse(color.hidden('_')); 76 + return styleText(['inverse', 'hidden'], '_'); 75 77 } 76 78 if (this._cursor >= this.userInput.length) { 77 79 return `${this.userInput}█`; 78 80 } 79 81 const s1 = this.userInput.slice(0, this._cursor); 80 82 const [s2, ...s3] = this.userInput.slice(this._cursor); 81 - return `${s1}${color.inverse(s2)}${s3.join('')}`; 83 + return `${s1}${styleText('inverse', s2)}${s3.join('')}`; 82 84 } 83 85 84 86 get options(): T[] { ··· 143 145 144 146 // Start navigation mode with up/down arrows 145 147 if (isUpKey || isDownKey) { 146 - this.#cursor = Math.max( 147 - 0, 148 - Math.min(this.#cursor + (isUpKey ? -1 : 1), this.filteredOptions.length - 1) 149 - ); 148 + this.#cursor = findCursor(this.#cursor, isUpKey ? -1 : 1, this.filteredOptions); 150 149 this.focusedValue = this.filteredOptions[this.#cursor]?.value; 151 150 if (!this.multiple) { 152 151 this.selectedValues = [this.focusedValue]; ··· 204 203 } else { 205 204 this.filteredOptions = [...options]; 206 205 } 207 - this.#cursor = getCursorForValue(this.focusedValue, this.filteredOptions); 208 - this.focusedValue = this.filteredOptions[this.#cursor]?.value; 206 + const valueCursor = getCursorForValue(this.focusedValue, this.filteredOptions); 207 + this.#cursor = findCursor(valueCursor, 0, this.filteredOptions); 208 + const focusedOption = this.filteredOptions[this.#cursor]; 209 + if (focusedOption && !focusedOption.disabled) { 210 + this.focusedValue = focusedOption.value; 211 + } else { 212 + this.focusedValue = undefined; 213 + } 209 214 if (!this.multiple) { 210 215 if (this.focusedValue !== undefined) { 211 216 this.toggleSelected(this.focusedValue);
+3 -3
packages/core/src/prompts/password.ts
··· 1 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import Prompt, { type PromptOptions } from './prompt.js'; 3 3 4 4 export interface PasswordOptions extends PromptOptions<string, PasswordPrompt> { ··· 18 18 } 19 19 const userInput = this.userInput; 20 20 if (this.cursor >= userInput.length) { 21 - return `${this.masked}${color.inverse(color.hidden('_'))}`; 21 + return `${this.masked}${styleText(['inverse', 'hidden'], '_')}`; 22 22 } 23 23 const masked = this.masked; 24 24 const s1 = masked.slice(0, this.cursor); 25 25 const s2 = masked.slice(this.cursor); 26 - return `${s1}${color.inverse(s2[0])}${s2.slice(1)}`; 26 + return `${s1}${styleText('inverse', s2[0])}${s2.slice(1)}`; 27 27 } 28 28 clear() { 29 29 this._clearUserInput();
+1 -1
packages/core/src/prompts/spinner.ts
··· 1 + import { settings } from '../utils/index.js'; 1 2 import Prompt, { type PromptOptions } from './prompt.js'; 2 - import {settings} from '../utils/index.js'; 3 3 4 4 const removeTrailingDots = (msg: string): string => { 5 5 return msg.replace(/\.+$/, '');
+2 -2
packages/core/src/prompts/text.ts
··· 1 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import Prompt, { type PromptOptions } from './prompt.js'; 3 3 4 4 export interface TextOptions extends PromptOptions<string, TextPrompt> { ··· 17 17 } 18 18 const s1 = userInput.slice(0, this.cursor); 19 19 const [s2, ...s3] = userInput.slice(this.cursor); 20 - return `${s1}${color.inverse(s2)}${s3.join('')}`; 20 + return `${s1}${styleText('inverse', s2)}${s3.join('')}`; 21 21 } 22 22 get cursor() { 23 23 return this._cursor;
+4
packages/core/src/utils/cursor.ts
··· 3 3 delta: number, 4 4 options: T[] 5 5 ) { 6 + const hasEnabledOptions = options.some((opt) => !opt.disabled); 7 + if (!hasEnabledOptions) { 8 + return cursor; 9 + } 6 10 const newCursor = cursor + delta; 7 11 const maxCursor = Math.max(options.length - 1, 0); 8 12 const clampedCursor = newCursor < 0 ? maxCursor : newCursor > maxCursor ? 0 : newCursor;
+4 -4
packages/core/test/prompts/password.test.ts
··· 1 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import { cursor } from 'sisteransi'; 3 3 import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 4 import { default as PasswordPrompt } from '../../src/prompts/password.js'; ··· 65 65 }); 66 66 instance.prompt(); 67 67 input.emit('keypress', 'x', { name: 'x' }); 68 - expect(instance.userInputWithCursor).to.equal(`•${color.inverse(color.hidden('_'))}`); 68 + expect(instance.userInputWithCursor).to.equal(`•${styleText(['inverse', 'hidden'], '_')}`); 69 69 }); 70 70 71 71 test('renders cursor inside value', () => { ··· 80 80 input.emit('keypress', 'z', { name: 'z' }); 81 81 input.emit('keypress', 'left', { name: 'left' }); 82 82 input.emit('keypress', 'left', { name: 'left' }); 83 - expect(instance.userInputWithCursor).to.equal(`•${color.inverse('•')}•`); 83 + expect(instance.userInputWithCursor).to.equal(`•${styleText('inverse', '•')}•`); 84 84 }); 85 85 86 86 test('renders custom mask', () => { ··· 92 92 }); 93 93 instance.prompt(); 94 94 input.emit('keypress', 'x', { name: 'x' }); 95 - expect(instance.userInputWithCursor).to.equal(`X${color.inverse(color.hidden('_'))}`); 95 + expect(instance.userInputWithCursor).to.equal(`X${styleText(['inverse', 'hidden'], '_')}`); 96 96 }); 97 97 }); 98 98 });
+2 -2
packages/core/test/prompts/text.test.ts
··· 1 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import { cursor } from 'sisteransi'; 3 3 import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; 4 4 import { default as TextPrompt } from '../../src/prompts/text.js'; ··· 93 93 input.emit('keypress', keys[i], { name: keys[i] }); 94 94 } 95 95 input.emit('keypress', 'left', { name: 'left' }); 96 - expect(instance.userInputWithCursor).to.equal(`fo${color.inverse('o')}`); 96 + expect(instance.userInputWithCursor).to.equal(`fo${styleText('inverse', 'o')}`); 97 97 }); 98 98 99 99 test('shows cursor at end if beyond value', () => {
+2 -8
packages/core/test/utils.test.ts
··· 14 14 test('clears output on keypress', () => { 15 15 const input = new MockReadable(); 16 16 const output = new MockWritable(); 17 - // @ts-ignore 18 17 const callback = block({ input, output }); 19 18 20 19 const event: Key = { ··· 29 28 test('clears output vertically when return pressed', () => { 30 29 const input = new MockReadable(); 31 30 const output = new MockWritable(); 32 - // @ts-ignore 33 31 const callback = block({ input, output }); 34 32 35 33 const event: Key = { ··· 44 42 test('ignores additional keypresses after dispose', () => { 45 43 const input = new MockReadable(); 46 44 const output = new MockWritable(); 47 - // @ts-ignore 48 45 const callback = block({ input, output }); 49 46 50 47 const event: Key = { ··· 61 58 const input = new MockReadable(); 62 59 const output = new MockWritable(); 63 60 // purposely don't keep the callback since we would exit the process 64 - // @ts-ignore 65 61 block({ input, output }); 66 - // @ts-ignore 67 - const spy = vi.spyOn(process, 'exit').mockImplementation(() => { 62 + const spy = vi.spyOn(process, 'exit').mockImplementation((() => { 68 63 return; 69 - }); 64 + }) as () => never); 70 65 71 66 const event: Key = { 72 67 name: 'c', ··· 80 75 test('does not clear if overwrite=false', () => { 81 76 const input = new MockReadable(); 82 77 const output = new MockWritable(); 83 - // @ts-ignore 84 78 const callback = block({ input, output, overwrite: false }); 85 79 86 80 const event: Key = {
+18
packages/prompts/CHANGELOG.md
··· 1 1 # @clack/prompts 2 2 3 + ## 1.0.1 4 + 5 + ### Patch Changes 6 + 7 + - 6404dc1: Disallows selection of `disabled` options in autocomplete. 8 + - 86e36d8: Adds `withGuide` support to select prompt. 9 + - c697439: Fixes line wrapping behavior in autocomplete. 10 + - 0ded19c: Simplifies `withGuide` option checks. 11 + - 0e4ddc9: Fixes `withGuide` support in password and path prompts. 12 + - 76550d6: Adds `withGuide` support to selectKey prompt. 13 + - f9b9953: Adds `withGuide` support to password prompt. 14 + - 0e93ccb: Adds `vertical` arrangement option to `confirm` prompt. 15 + - 4e9ae13: Adds `withGuide` support to confirm prompt. 16 + - 0256238: Adds `withGuide` support to spinner prompt. 17 + - Updated dependencies [6404dc1] 18 + - Updated dependencies [2533180] 19 + - @clack/core@1.0.1 20 + 3 21 ## 1.0.0 4 22 5 23 ### Major Changes
+1 -2
packages/prompts/package.json
··· 1 1 { 2 2 "name": "@clack/prompts", 3 - "version": "1.0.0", 3 + "version": "1.0.1", 4 4 "type": "module", 5 5 "main": "./dist/index.mjs", 6 6 "module": "./dist/index.mjs", ··· 54 54 }, 55 55 "dependencies": { 56 56 "@clack/core": "workspace:*", 57 - "picocolors": "^1.0.0", 58 57 "sisteransi": "^1.0.5" 59 58 }, 60 59 "devDependencies": {
+78 -52
packages/prompts/src/autocomplete.ts
··· 1 - import { AutocompletePrompt } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { AutocompletePrompt, settings } from '@clack/core'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 95 95 output: opts.output, 96 96 validate: opts.validate, 97 97 render() { 98 + const hasGuide = opts.withGuide ?? settings.withGuide; 98 99 // Title and message display 99 - const headings = [`${color.gray(S_BAR)}`, `${symbol(this.state)} ${opts.message}`]; 100 + const headings = hasGuide 101 + ? [`${styleText('gray', S_BAR)}`, `${symbol(this.state)} ${opts.message}`] 102 + : [`${symbol(this.state)} ${opts.message}`]; 100 103 const userInput = this.userInput; 101 104 const options = this.options; 102 105 const placeholder = opts.placeholder; 103 106 const showPlaceholder = userInput === '' && placeholder !== undefined; 107 + const opt = (option: Option<Value>, state: 'inactive' | 'active' | 'disabled') => { 108 + const label = getLabel(option); 109 + const hint = 110 + option.hint && option.value === this.focusedValue 111 + ? styleText('dim', ` (${option.hint})`) 112 + : ''; 113 + switch (state) { 114 + case 'active': 115 + return `${styleText('green', S_RADIO_ACTIVE)} ${label}${hint}`; 116 + case 'inactive': 117 + return `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', label)}`; 118 + case 'disabled': 119 + return `${styleText('gray', S_RADIO_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`; 120 + } 121 + }; 104 122 105 123 // Handle different states 106 124 switch (this.state) { ··· 108 126 // Show selected value 109 127 const selected = getSelectedOptions(this.selectedValues, options); 110 128 const label = 111 - selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : ''; 112 - return `${headings.join('\n')}\n${color.gray(S_BAR)}${label}`; 129 + selected.length > 0 ? ` ${styleText('dim', selected.map(getLabel).join(', '))}` : ''; 130 + const submitPrefix = hasGuide ? styleText('gray', S_BAR) : ''; 131 + return `${headings.join('\n')}\n${submitPrefix}${label}`; 113 132 } 114 133 115 134 case 'cancel': { 116 - const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : ''; 117 - return `${headings.join('\n')}\n${color.gray(S_BAR)}${userInputText}`; 135 + const userInputText = userInput 136 + ? ` ${styleText(['strikethrough', 'dim'], userInput)}` 137 + : ''; 138 + const cancelPrefix = hasGuide ? styleText('gray', S_BAR) : ''; 139 + return `${headings.join('\n')}\n${cancelPrefix}${userInputText}`; 118 140 } 119 141 120 142 default: { 121 - const guidePrefix = `${(this.state === 'error' ? color.yellow : color.cyan)(S_BAR)} `; 122 - const guidePrefixEnd = (this.state === 'error' ? color.yellow : color.cyan)(S_BAR_END); 143 + const barStyle = this.state === 'error' ? 'yellow' : 'cyan'; 144 + const guidePrefix = hasGuide ? `${styleText(barStyle, S_BAR)} ` : ''; 145 + const guidePrefixEnd = hasGuide ? styleText(barStyle, S_BAR_END) : ''; 123 146 // Display cursor position - show plain text in navigation mode 124 147 let searchText = ''; 125 148 if (this.isNavigating || showPlaceholder) { 126 149 const searchTextValue = showPlaceholder ? placeholder : userInput; 127 - searchText = searchTextValue !== '' ? ` ${color.dim(searchTextValue)}` : ''; 150 + searchText = searchTextValue !== '' ? ` ${styleText('dim', searchTextValue)}` : ''; 128 151 } else { 129 152 searchText = ` ${this.userInputWithCursor}`; 130 153 } ··· 132 155 // Show match count if filtered 133 156 const matches = 134 157 this.filteredOptions.length !== options.length 135 - ? color.dim( 158 + ? styleText( 159 + 'dim', 136 160 ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` 137 161 ) 138 162 : ''; ··· 140 164 // No matches message 141 165 const noResults = 142 166 this.filteredOptions.length === 0 && userInput 143 - ? [`${guidePrefix}${color.yellow('No matches found')}`] 167 + ? [`${guidePrefix}${styleText('yellow', 'No matches found')}`] 144 168 : []; 145 169 146 170 const validationError = 147 - this.state === 'error' ? [`${guidePrefix}${color.yellow(this.error)}`] : []; 171 + this.state === 'error' ? [`${guidePrefix}${styleText('yellow', this.error)}`] : []; 148 172 173 + if (hasGuide) { 174 + headings.push(`${guidePrefix.trimEnd()}`); 175 + } 149 176 headings.push( 150 - `${guidePrefix.trimEnd()}`, 151 - `${guidePrefix}${color.dim('Search:')}${searchText}${matches}`, 177 + `${guidePrefix}${styleText('dim', 'Search:')}${searchText}${matches}`, 152 178 ...noResults, 153 179 ...validationError 154 180 ); 155 181 156 182 // Show instructions 157 183 const instructions = [ 158 - `${color.dim('↑/↓')} to select`, 159 - `${color.dim('Enter:')} confirm`, 160 - `${color.dim('Type:')} to search`, 184 + `${styleText('dim', '↑/↓')} to select`, 185 + `${styleText('dim', 'Enter:')} confirm`, 186 + `${styleText('dim', 'Type:')} to search`, 161 187 ]; 162 188 163 - const footers = [ 164 - `${guidePrefix}${color.dim(instructions.join(' • '))}`, 165 - `${guidePrefixEnd}`, 166 - ]; 189 + const footers = [`${guidePrefix}${instructions.join(' • ')}`, guidePrefixEnd]; 167 190 168 191 // Render options with selection 169 192 const displayOptions = ··· 172 195 : limitOptions({ 173 196 cursor: this.cursor, 174 197 options: this.filteredOptions, 175 - columnPadding: 3, // for `| ` 198 + columnPadding: hasGuide ? 3 : 0, // for `| ` when guide is shown 176 199 rowPadding: headings.length + footers.length, 177 200 style: (option, active) => { 178 - const label = getLabel(option); 179 - const hint = 180 - option.hint && option.value === this.focusedValue 181 - ? color.dim(` (${option.hint})`) 182 - : ''; 183 - 184 - return active 185 - ? `${color.green(S_RADIO_ACTIVE)} ${label}${hint}` 186 - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}${hint}`; 201 + return opt( 202 + option, 203 + option.disabled ? 'disabled' : active ? 'active' : 'inactive' 204 + ); 187 205 }, 188 206 maxItems: opts.maxItems, 189 207 output: opts.output, ··· 230 248 const label = option.label ?? String(option.value ?? ''); 231 249 const hint = 232 250 option.hint && focusedValue !== undefined && option.value === focusedValue 233 - ? color.dim(` (${option.hint})`) 251 + ? styleText('dim', ` (${option.hint})`) 234 252 : ''; 235 - const checkbox = isSelected ? color.green(S_CHECKBOX_SELECTED) : color.dim(S_CHECKBOX_INACTIVE); 253 + const checkbox = isSelected 254 + ? styleText('green', S_CHECKBOX_SELECTED) 255 + : styleText('dim', S_CHECKBOX_INACTIVE); 236 256 257 + if (option.disabled) { 258 + return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${styleText(['strikethrough', 'gray'], label)}`; 259 + } 237 260 if (active) { 238 261 return `${checkbox} ${label}${hint}`; 239 262 } 240 - return `${checkbox} ${color.dim(label)}`; 263 + return `${checkbox} ${styleText('dim', label)}`; 241 264 }; 242 265 243 266 // Create text prompt which we'll use as foundation ··· 261 284 output: opts.output, 262 285 render() { 263 286 // Title and symbol 264 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 287 + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 265 288 266 289 // Selection counter 267 290 const userInput = this.userInput; ··· 271 294 // Search input display 272 295 const searchText = 273 296 this.isNavigating || showPlaceholder 274 - ? color.dim(showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode 297 + ? styleText('dim', showPlaceholder ? placeholder : userInput) // Just show plain text when in navigation mode 275 298 : this.userInputWithCursor; 276 299 277 300 const options = this.options; 278 301 279 302 const matches = 280 303 this.filteredOptions.length !== options.length 281 - ? color.dim( 304 + ? styleText( 305 + 'dim', 282 306 ` (${this.filteredOptions.length} match${this.filteredOptions.length === 1 ? '' : 'es'})` 283 307 ) 284 308 : ''; ··· 286 310 // Render prompt state 287 311 switch (this.state) { 288 312 case 'submit': { 289 - return `${title}${color.gray(S_BAR)} ${color.dim(`${this.selectedValues.length} items selected`)}`; 313 + return `${title}${styleText('gray', S_BAR)} ${styleText('dim', `${this.selectedValues.length} items selected`)}`; 290 314 } 291 315 case 'cancel': { 292 - return `${title}${color.gray(S_BAR)} ${color.strikethrough(color.dim(userInput))}`; 316 + return `${title}${styleText('gray', S_BAR)} ${styleText(['strikethrough', 'dim'], userInput)}`; 293 317 } 294 318 default: { 295 - const barColor = this.state === 'error' ? color.yellow : color.cyan; 319 + const barStyle = this.state === 'error' ? 'yellow' : 'cyan'; 296 320 // Instructions 297 321 const instructions = [ 298 - `${color.dim('↑/↓')} to navigate`, 299 - `${color.dim(this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`, 300 - `${color.dim('Enter:')} confirm`, 301 - `${color.dim('Type:')} to search`, 322 + `${styleText('dim', '↑/↓')} to navigate`, 323 + `${styleText('dim', this.isNavigating ? 'Space/Tab:' : 'Tab:')} select`, 324 + `${styleText('dim', 'Enter:')} confirm`, 325 + `${styleText('dim', 'Type:')} to search`, 302 326 ]; 303 327 304 328 // No results message 305 329 const noResults = 306 330 this.filteredOptions.length === 0 && userInput 307 - ? [`${barColor(S_BAR)} ${color.yellow('No matches found')}`] 331 + ? [`${styleText(barStyle, S_BAR)} ${styleText('yellow', 'No matches found')}`] 308 332 : []; 309 333 310 334 const errorMessage = 311 - this.state === 'error' ? [`${barColor(S_BAR)} ${color.yellow(this.error)}`] : []; 335 + this.state === 'error' 336 + ? [`${styleText(barStyle, S_BAR)} ${styleText('yellow', this.error)}`] 337 + : []; 312 338 313 339 // Calculate header and footer line counts for rowPadding 314 340 const headerLines = [ 315 - ...`${title}${barColor(S_BAR)}`.split('\n'), 316 - `${barColor(S_BAR)} ${color.dim('Search:')} ${searchText}${matches}`, 341 + ...`${title}${styleText(barStyle, S_BAR)}`.split('\n'), 342 + `${styleText(barStyle, S_BAR)} ${styleText('dim', 'Search:')} ${searchText}${matches}`, 317 343 ...noResults, 318 344 ...errorMessage, 319 345 ]; 320 346 const footerLines = [ 321 - `${barColor(S_BAR)} ${color.dim(instructions.join(' • '))}`, 322 - `${barColor(S_BAR_END)}`, 347 + `${styleText(barStyle, S_BAR)} ${instructions.join(' • ')}`, 348 + styleText(barStyle, S_BAR_END), 323 349 ]; 324 350 325 351 // Get limited options for display ··· 336 362 // Build the prompt display 337 363 return [ 338 364 ...headerLines, 339 - ...displayOptions.map((option) => `${barColor(S_BAR)} ${option}`), 365 + ...displayOptions.map((option) => `${styleText(barStyle, S_BAR)} ${option}`), 340 366 ...footerLines, 341 367 ].join('\n'); 342 368 }
+1 -1
packages/prompts/src/box.ts
··· 67 67 const titlePadding = opts?.titlePadding ?? 1; 68 68 const contentPadding = opts?.contentPadding ?? 2; 69 69 const width = opts?.width === undefined || opts.width === 'auto' ? 1 : Math.min(1, opts.width); 70 - const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false; 70 + const hasGuide = opts?.withGuide ?? settings.withGuide; 71 71 const linePrefix = !hasGuide ? '' : `${S_BAR} `; 72 72 const formatBorder = opts?.formatBorder ?? defaultFormatBorder; 73 73 const symbols = (opts?.rounded ? roundedSymbols : squareSymbols).map(formatBorder);
+9 -9
packages/prompts/src/common.ts
··· 1 1 import type { Readable, Writable } from 'node:stream'; 2 + import { styleText } from 'node:util'; 2 3 import type { State } from '@clack/core'; 3 4 import isUnicodeSupported from 'is-unicode-supported'; 4 - import color from 'picocolors'; 5 5 6 6 export const unicode = isUnicodeSupported(); 7 7 export const isCI = (): boolean => process.env.CI === 'true'; ··· 43 43 switch (state) { 44 44 case 'initial': 45 45 case 'active': 46 - return color.cyan(S_STEP_ACTIVE); 46 + return styleText('cyan', S_STEP_ACTIVE); 47 47 case 'cancel': 48 - return color.red(S_STEP_CANCEL); 48 + return styleText('red', S_STEP_CANCEL); 49 49 case 'error': 50 - return color.yellow(S_STEP_ERROR); 50 + return styleText('yellow', S_STEP_ERROR); 51 51 case 'submit': 52 - return color.green(S_STEP_SUBMIT); 52 + return styleText('green', S_STEP_SUBMIT); 53 53 } 54 54 }; 55 55 ··· 57 57 switch (state) { 58 58 case 'initial': 59 59 case 'active': 60 - return color.cyan(S_BAR); 60 + return styleText('cyan', S_BAR); 61 61 case 'cancel': 62 - return color.red(S_BAR); 62 + return styleText('red', S_BAR); 63 63 case 'error': 64 - return color.yellow(S_BAR); 64 + return styleText('yellow', S_BAR); 65 65 case 'submit': 66 - return color.green(S_BAR); 66 + return styleText('green', S_BAR); 67 67 } 68 68 }; 69 69
+24 -16
packages/prompts/src/confirm.ts
··· 1 - import { ConfirmPrompt } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { ConfirmPrompt, settings } from '@clack/core'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 14 14 active?: string; 15 15 inactive?: string; 16 16 initialValue?: boolean; 17 + vertical?: boolean; 17 18 } 18 19 export const confirm = (opts: ConfirmOptions) => { 19 20 const active = opts.active ?? 'Yes'; ··· 26 27 output: opts.output, 27 28 initialValue: opts.initialValue ?? true, 28 29 render() { 29 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 30 + const hasGuide = opts.withGuide ?? settings.withGuide; 31 + const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} ${opts.message}\n`; 30 32 const value = this.value ? active : inactive; 31 33 32 34 switch (this.state) { 33 - case 'submit': 34 - return `${title}${color.gray(S_BAR)} ${color.dim(value)}`; 35 - case 'cancel': 36 - return `${title}${color.gray(S_BAR)} ${color.strikethrough( 37 - color.dim(value) 38 - )}\n${color.gray(S_BAR)}`; 35 + case 'submit': { 36 + const submitPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 37 + return `${title}${submitPrefix}${styleText('dim', value)}`; 38 + } 39 + case 'cancel': { 40 + const cancelPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 41 + return `${title}${cancelPrefix}${styleText(['strikethrough', 'dim'], value)}${ 42 + hasGuide ? `\n${styleText('gray', S_BAR)}` : '' 43 + }`; 44 + } 39 45 default: { 40 - return `${title}${color.cyan(S_BAR)} ${ 46 + const defaultPrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; 47 + const defaultPrefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : ''; 48 + return `${title}${defaultPrefix}${ 41 49 this.value 42 - ? `${color.green(S_RADIO_ACTIVE)} ${active}` 43 - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(active)}` 44 - } ${color.dim('/')} ${ 50 + ? `${styleText('green', S_RADIO_ACTIVE)} ${active}` 51 + : `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', active)}` 52 + }${opts.vertical ? (hasGuide ? `\n${styleText('cyan', S_BAR)} ` : '\n') : ` ${styleText('dim', '/')} `}${ 45 53 !this.value 46 - ? `${color.green(S_RADIO_ACTIVE)} ${inactive}` 47 - : `${color.dim(S_RADIO_INACTIVE)} ${color.dim(inactive)}` 48 - }\n${color.cyan(S_BAR_END)}\n`; 54 + ? `${styleText('green', S_RADIO_ACTIVE)} ${inactive}` 55 + : `${styleText('dim', S_RADIO_INACTIVE)} ${styleText('dim', inactive)}` 56 + }\n${defaultPrefixEnd}\n`; 49 57 } 50 58 } 51 59 },
+35 -30
packages/prompts/src/group-multi-select.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import { GroupMultiSelectPrompt } from '@clack/core'; 2 - import color from 'picocolors'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 42 42 const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; 43 43 let spacingPrefix = ''; 44 44 if (groupSpacing > 0 && !isItem) { 45 - const spacingPrefixText = `\n${color.cyan(S_BAR)}`; 45 + const spacingPrefixText = `\n${styleText('cyan', S_BAR)}`; 46 46 spacingPrefix = `${spacingPrefixText.repeat(groupSpacing - 1)}${spacingPrefixText} `; 47 47 } 48 48 49 49 if (state === 'active') { 50 - return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ 51 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 50 + return `${spacingPrefix}${styleText('dim', prefix)}${styleText('cyan', S_CHECKBOX_ACTIVE)} ${label}${ 51 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 52 52 }`; 53 53 } 54 54 if (state === 'group-active') { 55 - return `${spacingPrefix}${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; 55 + return `${spacingPrefix}${prefix}${styleText('cyan', S_CHECKBOX_ACTIVE)} ${styleText('dim', label)}`; 56 56 } 57 57 if (state === 'group-active-selected') { 58 - return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; 58 + return `${spacingPrefix}${prefix}${styleText('green', S_CHECKBOX_SELECTED)} ${styleText('dim', label)}`; 59 59 } 60 60 if (state === 'selected') { 61 - const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : ''; 62 - return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)}${ 63 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 61 + const selectedCheckbox = 62 + isItem || selectableGroups ? styleText('green', S_CHECKBOX_SELECTED) : ''; 63 + return `${spacingPrefix}${styleText('dim', prefix)}${selectedCheckbox} ${styleText('dim', label)}${ 64 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 64 65 }`; 65 66 } 66 67 if (state === 'cancelled') { 67 - return `${color.strikethrough(color.dim(label))}`; 68 + return `${styleText(['strikethrough', 'dim'], label)}`; 68 69 } 69 70 if (state === 'active-selected') { 70 - return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label}${ 71 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 71 + return `${spacingPrefix}${styleText('dim', prefix)}${styleText('green', S_CHECKBOX_SELECTED)} ${label}${ 72 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 72 73 }`; 73 74 } 74 75 if (state === 'submitted') { 75 - return `${color.dim(label)}`; 76 + return `${styleText('dim', label)}`; 76 77 } 77 - const unselectedCheckbox = isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ''; 78 - return `${spacingPrefix}${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; 78 + const unselectedCheckbox = 79 + isItem || selectableGroups ? styleText('dim', S_CHECKBOX_INACTIVE) : ''; 80 + return `${spacingPrefix}${styleText('dim', prefix)}${unselectedCheckbox} ${styleText('dim', label)}`; 79 81 }; 80 82 const required = opts.required ?? true; 81 83 ··· 90 92 selectableGroups, 91 93 validate(selected: Value[] | undefined) { 92 94 if (required && (selected === undefined || selected.length === 0)) 93 - return `Please select at least one option.\n${color.reset( 94 - color.dim( 95 - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( 96 - color.bgWhite(color.inverse(' enter ')) 95 + return `Please select at least one option.\n${styleText( 96 + 'reset', 97 + styleText( 98 + 'dim', 99 + `Press ${styleText(['gray', 'bgWhite', 'inverse'], ' space ')} to select, ${styleText( 100 + 'gray', 101 + styleText(['bgWhite', 'inverse'], ' enter ') 97 102 )} to submit` 98 103 ) 99 104 )}`; 100 105 }, 101 106 render() { 102 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 107 + const title = `${styleText('gray', S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 103 108 const value = this.value ?? []; 104 109 105 110 switch (this.state) { ··· 108 113 .filter(({ value: optionValue }) => value.includes(optionValue)) 109 114 .map((option) => opt(option, 'submitted')); 110 115 const optionsText = 111 - selectedOptions.length === 0 ? '' : ` ${selectedOptions.join(color.dim(', '))}`; 112 - return `${title}${color.gray(S_BAR)}${optionsText}`; 116 + selectedOptions.length === 0 ? '' : ` ${selectedOptions.join(styleText('dim', ', '))}`; 117 + return `${title}${styleText('gray', S_BAR)}${optionsText}`; 113 118 } 114 119 case 'cancel': { 115 120 const label = this.options 116 121 .filter(({ value: optionValue }) => value.includes(optionValue)) 117 122 .map((option) => opt(option, 'cancelled')) 118 - .join(color.dim(', ')); 119 - return `${title}${color.gray(S_BAR)} ${ 120 - label.trim() ? `${label}\n${color.gray(S_BAR)}` : '' 123 + .join(styleText('dim', ', ')); 124 + return `${title}${styleText('gray', S_BAR)} ${ 125 + label.trim() ? `${label}\n${styleText('gray', S_BAR)}` : '' 121 126 }`; 122 127 } 123 128 case 'error': { 124 129 const footer = this.error 125 130 .split('\n') 126 131 .map((ln, i) => 127 - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` 132 + i === 0 ? `${styleText('yellow', S_BAR_END)} ${styleText('yellow', ln)}` : ` ${ln}` 128 133 ) 129 134 .join('\n'); 130 - return `${title}${color.yellow(S_BAR)} ${this.options 135 + return `${title}${styleText('yellow', S_BAR)} ${this.options 131 136 .map((option, i, options) => { 132 137 const selected = 133 138 value.includes(option.value) || ··· 148 153 } 149 154 return opt(option, active ? 'active' : 'inactive', options); 150 155 }) 151 - .join(`\n${color.yellow(S_BAR)} `)}\n${footer}\n`; 156 + .join(`\n${styleText('yellow', S_BAR)} `)}\n${footer}\n`; 152 157 } 153 158 default: { 154 159 const optionsText = this.options ··· 178 183 const prefix = i !== 0 && !optionText.startsWith('\n') ? ' ' : ''; 179 184 return `${prefix}${optionText}`; 180 185 }) 181 - .join(`\n${color.cyan(S_BAR)}`); 186 + .join(`\n${styleText('cyan', S_BAR)}`); 182 187 const optionsPrefix = optionsText.startsWith('\n') ? '' : ' '; 183 - return `${title}${color.cyan(S_BAR)}${optionsPrefix}${optionsText}\n${color.cyan(S_BAR_END)}\n`; 188 + return `${title}${styleText('cyan', S_BAR)}${optionsPrefix}${optionsText}\n${styleText('cyan', S_BAR_END)}\n`; 184 189 } 185 190 } 186 191 },
+24 -16
packages/prompts/src/limit-options.ts
··· 1 - import type { Writable } from 'node:stream'; 1 + import { styleText } from 'node:util'; 2 2 import { getColumns, getRows } from '@clack/core'; 3 3 import { wrapAnsi } from 'fast-wrap-ansi'; 4 - import color from 'picocolors'; 5 4 import type { CommonOptions } from './common.js'; 6 5 7 6 export interface LimitOptionsParams<TOption> extends CommonOptions { 8 7 options: TOption[]; 9 - maxItems: number | undefined; 10 8 cursor: number; 11 9 style: (option: TOption, active: boolean) => string; 10 + maxItems?: number; 12 11 columnPadding?: number; 13 12 rowPadding?: number; 14 13 } ··· 33 32 return { lineCount, removals }; 34 33 }; 35 34 36 - export const limitOptions = <TOption>(params: LimitOptionsParams<TOption>): string[] => { 37 - const { cursor, options, style } = params; 38 - const output: Writable = params.output ?? process.stdout; 35 + export const limitOptions = <TOption>({ 36 + cursor, 37 + options, 38 + style, 39 + output = process.stdout, 40 + maxItems = Number.POSITIVE_INFINITY, 41 + columnPadding = 0, 42 + rowPadding = 4, 43 + }: LimitOptionsParams<TOption>): string[] => { 39 44 const columns = getColumns(output); 40 - const columnPadding = params.columnPadding ?? 0; 41 - const rowPadding = params.rowPadding ?? 4; 42 45 const maxWidth = columns - columnPadding; 43 46 const rows = getRows(output); 44 - const overflowFormat = color.dim('...'); 47 + const overflowFormat = styleText('dim', '...'); 45 48 46 - const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; 47 49 const outputMaxItems = Math.max(rows - rowPadding, 0); 48 50 // We clamp to minimum 5 because anything less doesn't make sense UX wise 49 - const maxItems = Math.max(Math.min(paramMaxItems, outputMaxItems), 5); 51 + const computedMaxItems = Math.max(Math.min(maxItems, outputMaxItems), 5); 50 52 let slidingWindowLocation = 0; 51 53 52 - if (cursor >= maxItems - 3) { 53 - slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); 54 + if (cursor >= computedMaxItems - 3) { 55 + slidingWindowLocation = Math.max( 56 + Math.min(cursor - computedMaxItems + 3, options.length - computedMaxItems), 57 + 0 58 + ); 54 59 } 55 60 56 - let shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; 61 + let shouldRenderTopEllipsis = computedMaxItems < options.length && slidingWindowLocation > 0; 57 62 let shouldRenderBottomEllipsis = 58 - maxItems < options.length && slidingWindowLocation + maxItems < options.length; 63 + computedMaxItems < options.length && slidingWindowLocation + computedMaxItems < options.length; 59 64 60 - const slidingWindowLocationEnd = Math.min(slidingWindowLocation + maxItems, options.length); 65 + const slidingWindowLocationEnd = Math.min( 66 + slidingWindowLocation + computedMaxItems, 67 + options.length 68 + ); 61 69 const lineGroups: Array<string[]> = []; 62 70 let lineCount = 0; 63 71 if (shouldRenderTopEllipsis) {
+9 -9
packages/prompts/src/log.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import { settings } from '@clack/core'; 2 - import color from 'picocolors'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 20 20 message: ( 21 21 message: string | string[] = [], 22 22 { 23 - symbol = color.gray(S_BAR), 24 - secondarySymbol = color.gray(S_BAR), 23 + symbol = styleText('gray', S_BAR), 24 + secondarySymbol = styleText('gray', S_BAR), 25 25 output = process.stdout, 26 26 spacing = 1, 27 27 withGuide, 28 28 }: LogMessageOptions = {} 29 29 ) => { 30 30 const parts: string[] = []; 31 - const hasGuide = (withGuide ?? settings.withGuide) !== false; 31 + const hasGuide = withGuide ?? settings.withGuide; 32 32 const spacingString = !hasGuide ? '' : secondarySymbol; 33 33 const prefix = !hasGuide ? '' : `${symbol} `; 34 34 const secondaryPrefix = !hasGuide ? '' : `${secondarySymbol} `; ··· 56 56 output.write(`${parts.join('\n')}\n`); 57 57 }, 58 58 info: (message: string, opts?: LogMessageOptions) => { 59 - log.message(message, { ...opts, symbol: color.blue(S_INFO) }); 59 + log.message(message, { ...opts, symbol: styleText('blue', S_INFO) }); 60 60 }, 61 61 success: (message: string, opts?: LogMessageOptions) => { 62 - log.message(message, { ...opts, symbol: color.green(S_SUCCESS) }); 62 + log.message(message, { ...opts, symbol: styleText('green', S_SUCCESS) }); 63 63 }, 64 64 step: (message: string, opts?: LogMessageOptions) => { 65 - log.message(message, { ...opts, symbol: color.green(S_STEP_SUBMIT) }); 65 + log.message(message, { ...opts, symbol: styleText('green', S_STEP_SUBMIT) }); 66 66 }, 67 67 warn: (message: string, opts?: LogMessageOptions) => { 68 - log.message(message, { ...opts, symbol: color.yellow(S_WARN) }); 68 + log.message(message, { ...opts, symbol: styleText('yellow', S_WARN) }); 69 69 }, 70 70 /** alias for `log.warn()`. */ 71 71 warning: (message: string, opts?: LogMessageOptions) => { 72 72 log.warn(message, opts); 73 73 }, 74 74 error: (message: string, opts?: LogMessageOptions) => { 75 - log.message(message, { ...opts, symbol: color.red(S_ERROR) }); 75 + log.message(message, { ...opts, symbol: styleText('red', S_ERROR) }); 76 76 }, 77 77 };
+4 -4
packages/prompts/src/messages.ts
··· 1 1 import type { Writable } from 'node:stream'; 2 - import color from 'picocolors'; 2 + import { styleText } from 'node:util'; 3 3 import { type CommonOptions, S_BAR, S_BAR_END, S_BAR_START } from './common.js'; 4 4 5 5 export const cancel = (message = '', opts?: CommonOptions) => { 6 6 const output: Writable = opts?.output ?? process.stdout; 7 - output.write(`${color.gray(S_BAR_END)} ${color.red(message)}\n\n`); 7 + output.write(`${styleText('gray', S_BAR_END)} ${styleText('red', message)}\n\n`); 8 8 }; 9 9 10 10 export const intro = (title = '', opts?: CommonOptions) => { 11 11 const output: Writable = opts?.output ?? process.stdout; 12 - output.write(`${color.gray(S_BAR_START)} ${title}\n`); 12 + output.write(`${styleText('gray', S_BAR_START)} ${title}\n`); 13 13 }; 14 14 15 15 export const outro = (message = '', opts?: CommonOptions) => { 16 16 const output: Writable = opts?.output ?? process.stdout; 17 - output.write(`${color.gray(S_BAR)}\n${color.gray(S_BAR_END)} ${message}\n\n`); 17 + output.write(`${styleText('gray', S_BAR)}\n${styleText('gray', S_BAR_END)} ${message}\n\n`); 18 18 };
+34 -27
packages/prompts/src/multi-select.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import { MultiSelectPrompt, wrapTextWithPrefix } from '@clack/core'; 2 - import color from 'picocolors'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 42 42 ) => { 43 43 const label = option.label ?? String(option.value); 44 44 if (state === 'disabled') { 45 - return `${color.gray(S_CHECKBOX_INACTIVE)} ${computeLabel(label, (str) => color.strikethrough(color.gray(str)))}${ 46 - option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' 45 + return `${styleText('gray', S_CHECKBOX_INACTIVE)} ${computeLabel(label, (str) => styleText(['strikethrough', 'gray'], str))}${ 46 + option.hint ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}` : '' 47 47 }`; 48 48 } 49 49 if (state === 'active') { 50 - return `${color.cyan(S_CHECKBOX_ACTIVE)} ${label}${ 51 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 50 + return `${styleText('cyan', S_CHECKBOX_ACTIVE)} ${label}${ 51 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 52 52 }`; 53 53 } 54 54 if (state === 'selected') { 55 - return `${color.green(S_CHECKBOX_SELECTED)} ${computeLabel(label, color.dim)}${ 56 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 55 + return `${styleText('green', S_CHECKBOX_SELECTED)} ${computeLabel(label, (text) => styleText('dim', text))}${ 56 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 57 57 }`; 58 58 } 59 59 if (state === 'cancelled') { 60 - return `${computeLabel(label, (text) => color.strikethrough(color.dim(text)))}`; 60 + return `${computeLabel(label, (text) => styleText(['strikethrough', 'dim'], text))}`; 61 61 } 62 62 if (state === 'active-selected') { 63 - return `${color.green(S_CHECKBOX_SELECTED)} ${label}${ 64 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 63 + return `${styleText('green', S_CHECKBOX_SELECTED)} ${label}${ 64 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 65 65 }`; 66 66 } 67 67 if (state === 'submitted') { 68 - return `${computeLabel(label, color.dim)}`; 68 + return `${computeLabel(label, (text) => styleText('dim', text))}`; 69 69 } 70 - return `${color.dim(S_CHECKBOX_INACTIVE)} ${computeLabel(label, color.dim)}`; 70 + return `${styleText('dim', S_CHECKBOX_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`; 71 71 }; 72 72 const required = opts.required ?? true; 73 73 ··· 81 81 cursorAt: opts.cursorAt, 82 82 validate(selected: Value[] | undefined) { 83 83 if (required && (selected === undefined || selected.length === 0)) 84 - return `Please select at least one option.\n${color.reset( 85 - color.dim( 86 - `Press ${color.gray(color.bgWhite(color.inverse(' space ')))} to select, ${color.gray( 87 - color.bgWhite(color.inverse(' enter ')) 84 + return `Please select at least one option.\n${styleText( 85 + 'reset', 86 + styleText( 87 + 'dim', 88 + `Press ${styleText(['gray', 'bgWhite', 'inverse'], ' space ')} to select, ${styleText( 89 + 'gray', 90 + styleText('bgWhite', styleText('inverse', ' enter ')) 88 91 )} to submit` 89 92 ) 90 93 )}`; ··· 96 99 `${symbolBar(this.state)} `, 97 100 `${symbol(this.state)} ` 98 101 ); 99 - const title = `${color.gray(S_BAR)}\n${wrappedMessage}\n`; 102 + const title = `${styleText('gray', S_BAR)}\n${wrappedMessage}\n`; 100 103 const value = this.value ?? []; 101 104 102 105 const styleOption = (option: Option<Value>, active: boolean) => { ··· 119 122 this.options 120 123 .filter(({ value: optionValue }) => value.includes(optionValue)) 121 124 .map((option) => opt(option, 'submitted')) 122 - .join(color.dim(', ')) || color.dim('none'); 125 + .join(styleText('dim', ', ')) || styleText('dim', 'none'); 123 126 const wrappedSubmitText = wrapTextWithPrefix( 124 127 opts.output, 125 128 submitText, 126 - `${color.gray(S_BAR)} ` 129 + `${styleText('gray', S_BAR)} ` 127 130 ); 128 131 return `${title}${wrappedSubmitText}`; 129 132 } ··· 131 134 const label = this.options 132 135 .filter(({ value: optionValue }) => value.includes(optionValue)) 133 136 .map((option) => opt(option, 'cancelled')) 134 - .join(color.dim(', ')); 137 + .join(styleText('dim', ', ')); 135 138 if (label.trim() === '') { 136 - return `${title}${color.gray(S_BAR)}`; 139 + return `${title}${styleText('gray', S_BAR)}`; 137 140 } 138 - const wrappedLabel = wrapTextWithPrefix(opts.output, label, `${color.gray(S_BAR)} `); 139 - return `${title}${wrappedLabel}\n${color.gray(S_BAR)}`; 141 + const wrappedLabel = wrapTextWithPrefix( 142 + opts.output, 143 + label, 144 + `${styleText('gray', S_BAR)} ` 145 + ); 146 + return `${title}${wrappedLabel}\n${styleText('gray', S_BAR)}`; 140 147 } 141 148 case 'error': { 142 - const prefix = `${color.yellow(S_BAR)} `; 149 + const prefix = `${styleText('yellow', S_BAR)} `; 143 150 const footer = this.error 144 151 .split('\n') 145 152 .map((ln, i) => 146 - i === 0 ? `${color.yellow(S_BAR_END)} ${color.yellow(ln)}` : ` ${ln}` 153 + i === 0 ? `${styleText('yellow', S_BAR_END)} ${styleText('yellow', ln)}` : ` ${ln}` 147 154 ) 148 155 .join('\n'); 149 156 // Calculate rowPadding: title lines + footer lines (error message + trailing newline) ··· 160 167 }).join(`\n${prefix}`)}\n${footer}\n`; 161 168 } 162 169 default: { 163 - const prefix = `${color.cyan(S_BAR)} `; 170 + const prefix = `${styleText('cyan', S_BAR)} `; 164 171 // Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline) 165 172 const titleLineCount = title.split('\n').length; 166 173 const footerLineCount = 2; // S_BAR_END + trailing newline ··· 172 179 columnPadding: prefix.length, 173 180 rowPadding: titleLineCount + footerLineCount, 174 181 style: styleOption, 175 - }).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`; 182 + }).join(`\n${prefix}`)}\n${styleText('cyan', S_BAR_END)}\n`; 176 183 } 177 184 } 178 185 },
+9 -7
packages/prompts/src/note.ts
··· 1 1 import process from 'node:process'; 2 2 import type { Writable } from 'node:stream'; 3 + import { styleText } from 'node:util'; 3 4 import { getColumns, settings } from '@clack/core'; 4 5 import stringWidth from 'fast-string-width'; 5 6 import { type Options as WrapAnsiOptions, wrapAnsi } from 'fast-wrap-ansi'; 6 - import color from 'picocolors'; 7 7 import { 8 8 type CommonOptions, 9 9 S_BAR, ··· 20 20 format?: FormatFn; 21 21 } 22 22 23 - const defaultNoteFormatter = (line: string): string => color.dim(line); 23 + const defaultNoteFormatter = (line: string): string => styleText('dim', line); 24 24 25 25 const wrapWithFormat = (message: string, width: number, format: FormatFn): string => { 26 26 const opts: WrapAnsiOptions = { ··· 36 36 37 37 export const note = (message = '', title = '', opts?: NoteOptions) => { 38 38 const output: Writable = opts?.output ?? process.stdout; 39 - const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false; 39 + const hasGuide = opts?.withGuide ?? settings.withGuide; 40 40 const format = opts?.format ?? defaultNoteFormatter; 41 41 const wrapMsg = wrapWithFormat(message, getColumns(output) - 6, format); 42 42 const lines = ['', ...wrapMsg.split('\n').map(format), '']; ··· 51 51 ) + 2; 52 52 const msg = lines 53 53 .map( 54 - (ln) => `${color.gray(S_BAR)} ${ln}${' '.repeat(len - stringWidth(ln))}${color.gray(S_BAR)}` 54 + (ln) => 55 + `${styleText('gray', S_BAR)} ${ln}${' '.repeat(len - stringWidth(ln))}${styleText('gray', S_BAR)}` 55 56 ) 56 57 .join('\n'); 57 - const leadingBorder = hasGuide ? `${color.gray(S_BAR)}\n` : ''; 58 + const leadingBorder = hasGuide ? `${styleText('gray', S_BAR)}\n` : ''; 58 59 const bottomLeft = hasGuide ? S_CONNECT_LEFT : S_CORNER_BOTTOM_LEFT; 59 60 output.write( 60 - `${leadingBorder}${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( 61 + `${leadingBorder}${styleText('green', S_STEP_SUBMIT)} ${styleText('reset', title)} ${styleText( 62 + 'gray', 61 63 S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT 62 - )}\n${msg}\n${color.gray(bottomLeft + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` 64 + )}\n${msg}\n${styleText('gray', bottomLeft + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` 63 65 ); 64 66 };
+20 -14
packages/prompts/src/password.ts
··· 1 - import { PasswordPrompt } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { PasswordPrompt, settings } from '@clack/core'; 3 3 import { type CommonOptions, S_BAR, S_BAR_END, S_PASSWORD_MASK, symbol } from './common.js'; 4 4 5 5 export interface PasswordOptions extends CommonOptions { ··· 16 16 input: opts.input, 17 17 output: opts.output, 18 18 render() { 19 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 19 + const hasGuide = opts.withGuide ?? settings.withGuide; 20 + const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} ${opts.message}\n`; 20 21 const userInput = this.userInputWithCursor; 21 22 const masked = this.masked; 22 23 23 24 switch (this.state) { 24 25 case 'error': { 25 - const maskedText = masked ? ` ${masked}` : ''; 26 + const errorPrefix = hasGuide ? `${styleText('yellow', S_BAR)} ` : ''; 27 + const errorPrefixEnd = hasGuide ? `${styleText('yellow', S_BAR_END)} ` : ''; 28 + const maskedText = masked ?? ''; 26 29 if (opts.clearOnError) { 27 30 this.clear(); 28 31 } 29 - return `${title.trim()}\n${color.yellow(S_BAR)}${maskedText}\n${color.yellow( 30 - S_BAR_END 31 - )} ${color.yellow(this.error)}\n`; 32 + return `${title.trim()}\n${errorPrefix}${maskedText}\n${errorPrefixEnd}${styleText('yellow', this.error)}\n`; 32 33 } 33 34 case 'submit': { 34 - const maskedText = masked ? ` ${color.dim(masked)}` : ''; 35 - return `${title}${color.gray(S_BAR)}${maskedText}`; 35 + const submitPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 36 + const maskedText = masked ? styleText('dim', masked) : ''; 37 + return `${title}${submitPrefix}${maskedText}`; 36 38 } 37 39 case 'cancel': { 38 - const maskedText = masked ? ` ${color.strikethrough(color.dim(masked))}` : ''; 39 - return `${title}${color.gray(S_BAR)}${maskedText}${ 40 - masked ? `\n${color.gray(S_BAR)}` : '' 40 + const cancelPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 41 + const maskedText = masked ? styleText(['strikethrough', 'dim'], masked) : ''; 42 + return `${title}${cancelPrefix}${maskedText}${ 43 + masked && hasGuide ? `\n${styleText('gray', S_BAR)}` : '' 41 44 }`; 42 45 } 43 - default: 44 - return `${title}${color.cyan(S_BAR)} ${userInput}\n${color.cyan(S_BAR_END)}\n`; 46 + default: { 47 + const defaultPrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; 48 + const defaultPrefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : ''; 49 + return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`; 50 + } 45 51 } 46 52 }, 47 53 }).prompt() as Promise<string | symbol>;
+6 -6
packages/prompts/src/progress-bar.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import type { State } from '@clack/core'; 2 - import color from 'picocolors'; 3 3 import { unicodeOr } from './common.js'; 4 4 import { type SpinnerOptions, type SpinnerResult, spinner } from './spinner.js'; 5 5 ··· 36 36 switch (state) { 37 37 case 'initial': 38 38 case 'active': 39 - return color.magenta; 39 + return (text: string) => styleText('magenta', text); 40 40 case 'error': 41 41 case 'cancel': 42 - return color.red; 42 + return (text: string) => styleText('red', text); 43 43 case 'submit': 44 - return color.green; 44 + return (text: string) => styleText('green', text); 45 45 default: 46 - return color.magenta; 46 + return (text: string) => styleText('magenta', text); 47 47 } 48 48 }; 49 49 const drawProgress = (state: State, msg: string) => { 50 50 const active = Math.floor((value / max) * size); 51 - return `${activeStyle(state)(S_PROGRESS_CHAR[style].repeat(active))}${color.dim(S_PROGRESS_CHAR[style].repeat(size - active))} ${msg}`; 51 + return `${activeStyle(state)(S_PROGRESS_CHAR[style].repeat(active))}${styleText('dim', S_PROGRESS_CHAR[style].repeat(size - active))} ${msg}`; 52 52 }; 53 53 54 54 const start = (msg = '') => {
+16 -14
packages/prompts/src/select-key.ts
··· 1 - import { SelectKeyPrompt, wrapTextWithPrefix } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { SelectKeyPrompt, settings, wrapTextWithPrefix } from '@clack/core'; 3 3 import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; 4 4 import type { Option } from './select.js'; 5 5 ··· 17 17 ) => { 18 18 const label = option.label ?? String(option.value); 19 19 if (state === 'selected') { 20 - return `${color.dim(label)}`; 20 + return `${styleText('dim', label)}`; 21 21 } 22 22 if (state === 'cancelled') { 23 - return `${color.strikethrough(color.dim(label))}`; 23 + return `${styleText(['strikethrough', 'dim'], label)}`; 24 24 } 25 25 if (state === 'active') { 26 - return `${color.bgCyan(color.gray(` ${option.value} `))} ${label}${ 27 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 26 + return `${styleText(['bgCyan', 'gray'], ` ${option.value} `)} ${label}${ 27 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 28 28 }`; 29 29 } 30 - return `${color.gray(color.bgWhite(color.inverse(` ${option.value} `)))} ${label}${ 31 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 30 + return `${styleText(['gray', 'bgWhite', 'inverse'], ` ${option.value} `)} ${label}${ 31 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 32 32 }`; 33 33 }; 34 34 ··· 40 40 initialValue: opts.initialValue, 41 41 caseSensitive: opts.caseSensitive, 42 42 render() { 43 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 43 + const hasGuide = opts.withGuide ?? settings.withGuide; 44 + const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} ${opts.message}\n`; 44 45 45 46 switch (this.state) { 46 47 case 'submit': { 47 - const submitPrefix = `${color.gray(S_BAR)} `; 48 + const submitPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 48 49 const selectedOption = 49 50 this.options.find((opt) => opt.value === this.value) ?? opts.options[0]; 50 51 const wrapped = wrapTextWithPrefix( ··· 55 56 return `${title}${wrapped}`; 56 57 } 57 58 case 'cancel': { 58 - const cancelPrefix = `${color.gray(S_BAR)} `; 59 + const cancelPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 59 60 const wrapped = wrapTextWithPrefix( 60 61 opts.output, 61 62 opt(this.options[0], 'cancelled'), 62 63 cancelPrefix 63 64 ); 64 - return `${title}${wrapped}\n${color.gray(S_BAR)}`; 65 + return `${title}${wrapped}${hasGuide ? `\n${styleText('gray', S_BAR)}` : ''}`; 65 66 } 66 67 default: { 67 - const defaultPrefix = `${color.cyan(S_BAR)} `; 68 + const defaultPrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; 69 + const defaultPrefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : ''; 68 70 const wrapped = this.options 69 71 .map((option, i) => 70 72 wrapTextWithPrefix( ··· 74 76 ) 75 77 ) 76 78 .join('\n'); 77 - return `${title}${wrapped}\n${color.cyan(S_BAR_END)}\n`; 79 + return `${title}${wrapped}\n${defaultPrefixEnd}\n`; 78 80 } 79 81 } 80 82 },
+18 -16
packages/prompts/src/select.ts
··· 1 - import { SelectPrompt, wrapTextWithPrefix } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { SelectPrompt, settings, wrapTextWithPrefix } from '@clack/core'; 3 3 import { 4 4 type CommonOptions, 5 5 S_BAR, ··· 90 90 const label = option.label ?? String(option.value); 91 91 switch (state) { 92 92 case 'disabled': 93 - return `${color.gray(S_RADIO_INACTIVE)} ${computeLabel(label, color.gray)}${ 94 - option.hint ? ` ${color.dim(`(${option.hint ?? 'disabled'})`)}` : '' 93 + return `${styleText('gray', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('gray', text))}${ 94 + option.hint ? ` ${styleText('dim', `(${option.hint ?? 'disabled'})`)}` : '' 95 95 }`; 96 96 case 'selected': 97 - return `${computeLabel(label, color.dim)}`; 97 + return `${computeLabel(label, (text) => styleText('dim', text))}`; 98 98 case 'active': 99 - return `${color.green(S_RADIO_ACTIVE)} ${label}${ 100 - option.hint ? ` ${color.dim(`(${option.hint})`)}` : '' 99 + return `${styleText('green', S_RADIO_ACTIVE)} ${label}${ 100 + option.hint ? ` ${styleText('dim', `(${option.hint})`)}` : '' 101 101 }`; 102 102 case 'cancelled': 103 - return `${computeLabel(label, (str) => color.strikethrough(color.dim(str)))}`; 103 + return `${computeLabel(label, (str) => styleText(['strikethrough', 'dim'], str))}`; 104 104 default: 105 - return `${color.dim(S_RADIO_INACTIVE)} ${computeLabel(label, color.dim)}`; 105 + return `${styleText('dim', S_RADIO_INACTIVE)} ${computeLabel(label, (text) => styleText('dim', text))}`; 106 106 } 107 107 }; 108 108 ··· 113 113 output: opts.output, 114 114 initialValue: opts.initialValue, 115 115 render() { 116 + const hasGuide = opts.withGuide ?? settings.withGuide; 116 117 const titlePrefix = `${symbol(this.state)} `; 117 118 const titlePrefixBar = `${symbolBar(this.state)} `; 118 119 const messageLines = wrapTextWithPrefix( ··· 121 122 titlePrefixBar, 122 123 titlePrefix 123 124 ); 124 - const title = `${color.gray(S_BAR)}\n${messageLines}\n`; 125 + const title = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${messageLines}\n`; 125 126 126 127 switch (this.state) { 127 128 case 'submit': { 128 - const submitPrefix = `${color.gray(S_BAR)} `; 129 + const submitPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 129 130 const wrappedLines = wrapTextWithPrefix( 130 131 opts.output, 131 132 opt(this.options[this.cursor], 'selected'), ··· 134 135 return `${title}${wrappedLines}`; 135 136 } 136 137 case 'cancel': { 137 - const cancelPrefix = `${color.gray(S_BAR)} `; 138 + const cancelPrefix = hasGuide ? `${styleText('gray', S_BAR)} ` : ''; 138 139 const wrappedLines = wrapTextWithPrefix( 139 140 opts.output, 140 141 opt(this.options[this.cursor], 'cancelled'), 141 142 cancelPrefix 142 143 ); 143 - return `${title}${wrappedLines}\n${color.gray(S_BAR)}`; 144 + return `${title}${wrappedLines}${hasGuide ? `\n${styleText('gray', S_BAR)}` : ''}`; 144 145 } 145 146 default: { 146 - const prefix = `${color.cyan(S_BAR)} `; 147 + const prefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; 148 + const prefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : ''; 147 149 // Calculate rowPadding: title lines + footer lines (S_BAR_END + trailing newline) 148 150 const titleLineCount = title.split('\n').length; 149 - const footerLineCount = 2; // S_BAR_END + trailing newline 151 + const footerLineCount = hasGuide ? 2 : 1; // S_BAR_END + trailing newline (or just trailing newline) 150 152 return `${title}${prefix}${limitOptions({ 151 153 output: opts.output, 152 154 cursor: this.cursor, ··· 156 158 rowPadding: titleLineCount + footerLineCount, 157 159 style: (item, active) => 158 160 opt(item, item.disabled ? 'disabled' : active ? 'active' : 'inactive'), 159 - }).join(`\n${prefix}`)}\n${color.cyan(S_BAR_END)}\n`; 161 + }).join(`\n${prefix}`)}\n${prefixEnd}\n`; 160 162 } 161 163 } 162 164 },
+10 -7
packages/prompts/src/spinner.ts
··· 1 - import { SpinnerPrompt } from '@clack/core'; 2 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 + import { SpinnerPrompt, settings } from '@clack/core'; 3 3 import { 4 4 type CommonOptions, 5 5 isCI as isCIFn, ··· 30 30 readonly isCancelled: boolean; 31 31 } 32 32 33 - const defaultStyleFn: SpinnerOptions['styleFrame'] = color.magenta; 33 + const defaultStyleFn: SpinnerOptions['styleFrame'] = (frame) => styleText('magenta', frame); 34 34 35 35 export const spinner = ({ 36 36 indicator = 'dots', ··· 54 54 signal: opts.signal, 55 55 input: opts.input, 56 56 render() { 57 + const hasGuide = opts.withGuide ?? settings.withGuide; 58 + 57 59 if (!this.isActive) { 58 60 if (this.silentExit) { 59 61 return ''; 60 62 } 61 63 const step = 62 64 this.exitCode === 0 63 - ? color.green(S_STEP_SUBMIT) 65 + ? styleText('green', S_STEP_SUBMIT) 64 66 : this.exitCode === 1 65 - ? color.red(S_STEP_CANCEL) 66 - : color.red(S_STEP_ERROR); 67 + ? styleText('red', S_STEP_CANCEL) 68 + : styleText('red', S_STEP_ERROR); 67 69 if (indicator === 'timer') { 68 70 return `${step} ${this.message} ${this.getFormattedTimer()}\n`; 69 71 } else { ··· 81 83 const loadingDots = '.'.repeat(Math.floor(this.indicatorTimer)).slice(0, 3); 82 84 outputMessage = `${frame} ${message}${loadingDots}`; 83 85 } 84 - return `${color.gray(S_BAR)}\n${outputMessage}`; 86 + const prefix = hasGuide ? `${styleText('grey', S_BAR)}\n` : ''; 87 + return `${prefix}${outputMessage}`; 85 88 }, 86 89 }); 87 90
+9 -10
packages/prompts/src/stream.ts
··· 1 - import { stripVTControlCharacters as strip } from 'node:util'; 2 - import color from 'picocolors'; 1 + import { stripVTControlCharacters as strip, styleText } from 'node:util'; 3 2 import { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT, S_SUCCESS, S_WARN } from './common.js'; 4 3 import type { LogMessageOptions } from './log.js'; 5 4 6 - const prefix = `${color.gray(S_BAR)} `; 5 + const prefix = `${styleText('gray', S_BAR)} `; 7 6 8 7 // TODO (43081j): this currently doesn't support custom `output` writables 9 8 // because we rely on `columns` existing (i.e. `process.stdout.columns). ··· 13 12 export const stream = { 14 13 message: async ( 15 14 iterable: Iterable<string> | AsyncIterable<string>, 16 - { symbol = color.gray(S_BAR) }: LogMessageOptions = {} 15 + { symbol = styleText('gray', S_BAR) }: LogMessageOptions = {} 17 16 ) => { 18 - process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `); 17 + process.stdout.write(`${styleText('gray', S_BAR)}\n${symbol} `); 19 18 let lineWidth = 3; 20 19 for await (let chunk of iterable) { 21 20 chunk = chunk.replace(/\n/g, `\n${prefix}`); ··· 34 33 process.stdout.write('\n'); 35 34 }, 36 35 info: (iterable: Iterable<string> | AsyncIterable<string>) => { 37 - return stream.message(iterable, { symbol: color.blue(S_INFO) }); 36 + return stream.message(iterable, { symbol: styleText('blue', S_INFO) }); 38 37 }, 39 38 success: (iterable: Iterable<string> | AsyncIterable<string>) => { 40 - return stream.message(iterable, { symbol: color.green(S_SUCCESS) }); 39 + return stream.message(iterable, { symbol: styleText('green', S_SUCCESS) }); 41 40 }, 42 41 step: (iterable: Iterable<string> | AsyncIterable<string>) => { 43 - return stream.message(iterable, { symbol: color.green(S_STEP_SUBMIT) }); 42 + return stream.message(iterable, { symbol: styleText('green', S_STEP_SUBMIT) }); 44 43 }, 45 44 warn: (iterable: Iterable<string> | AsyncIterable<string>) => { 46 - return stream.message(iterable, { symbol: color.yellow(S_WARN) }); 45 + return stream.message(iterable, { symbol: styleText('yellow', S_WARN) }); 47 46 }, 48 47 /** alias for `log.warn()`. */ 49 48 warning: (iterable: Iterable<string> | AsyncIterable<string>) => { 50 49 return stream.warn(iterable); 51 50 }, 52 51 error: (iterable: Iterable<string> | AsyncIterable<string>) => { 53 - return stream.message(iterable, { symbol: color.red(S_ERROR) }); 52 + return stream.message(iterable, { symbol: styleText('red', S_ERROR) }); 54 53 }, 55 54 };
+19 -13
packages/prompts/src/task-log.ts
··· 1 1 import type { Writable } from 'node:stream'; 2 + import { styleText } from 'node:util'; 2 3 import { getColumns } from '@clack/core'; 3 - import color from 'picocolors'; 4 4 import { erase } from 'sisteransi'; 5 5 import { 6 6 type CommonOptions, ··· 47 47 export const taskLog = (opts: TaskLogOptions) => { 48 48 const output: Writable = opts.output ?? process.stdout; 49 49 const columns = getColumns(output); 50 - const secondarySymbol = color.gray(S_BAR); 50 + const secondarySymbol = styleText('gray', S_BAR); 51 51 const spacing = opts.spacing ?? 1; 52 52 const barSize = 3; 53 53 const retainLog = opts.retainLog === true; 54 54 const isTTY = !isCIFn() && isTTYFn(output); 55 55 56 56 output.write(`${secondarySymbol}\n`); 57 - output.write(`${color.green(S_STEP_SUBMIT)} ${opts.title}\n`); 57 + output.write(`${styleText('green', S_STEP_SUBMIT)} ${opts.title}\n`); 58 58 for (let i = 0; i < spacing; i++) { 59 59 output.write(`${secondarySymbol}\n`); 60 60 } ··· 108 108 const printBuffer = (buffer: BufferEntry, messageSpacing?: number, full?: boolean): void => { 109 109 const messages = full ? `${buffer.full}\n${buffer.value}` : buffer.value; 110 110 if (buffer.header !== undefined && buffer.header !== '') { 111 - log.message(buffer.header.split('\n').map(color.bold), { 111 + log.message( 112 + buffer.header.split('\n').map((line) => styleText('bold', line)), 113 + { 114 + output, 115 + secondarySymbol, 116 + symbol: secondarySymbol, 117 + spacing: 0, 118 + } 119 + ); 120 + } 121 + log.message( 122 + messages.split('\n').map((line) => styleText('dim', line)), 123 + { 112 124 output, 113 125 secondarySymbol, 114 126 symbol: secondarySymbol, 115 - spacing: 0, 116 - }); 117 - } 118 - log.message(messages.split('\n').map(color.dim), { 119 - output, 120 - secondarySymbol, 121 - symbol: secondarySymbol, 122 - spacing: messageSpacing ?? spacing, 123 - }); 127 + spacing: messageSpacing ?? spacing, 128 + } 129 + ); 124 130 }; 125 131 const renderBuffer = (): void => { 126 132 for (const buffer of buffers) {
+14 -14
packages/prompts/src/text.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import { settings, TextPrompt } from '@clack/core'; 2 - import color from 'picocolors'; 3 3 import { type CommonOptions, S_BAR, S_BAR_END, symbol } from './common.js'; 4 4 5 5 export interface TextOptions extends CommonOptions { ··· 20 20 signal: opts.signal, 21 21 input: opts.input, 22 22 render() { 23 - const hasGuide = (opts?.withGuide ?? settings.withGuide) !== false; 24 - const titlePrefix = `${hasGuide ? `${color.gray(S_BAR)}\n` : ''}${symbol(this.state)} `; 23 + const hasGuide = opts?.withGuide ?? settings.withGuide; 24 + const titlePrefix = `${hasGuide ? `${styleText('gray', S_BAR)}\n` : ''}${symbol(this.state)} `; 25 25 const title = `${titlePrefix}${opts.message}\n`; 26 26 const placeholder = opts.placeholder 27 - ? color.inverse(opts.placeholder[0]) + color.dim(opts.placeholder.slice(1)) 28 - : color.inverse(color.hidden('_')); 27 + ? styleText('inverse', opts.placeholder[0]) + styleText('dim', opts.placeholder.slice(1)) 28 + : styleText(['inverse', 'hidden'], '_'); 29 29 const userInput = !this.userInput ? placeholder : this.userInputWithCursor; 30 30 const value = this.value ?? ''; 31 31 32 32 switch (this.state) { 33 33 case 'error': { 34 - const errorText = this.error ? ` ${color.yellow(this.error)}` : ''; 35 - const errorPrefix = hasGuide ? `${color.yellow(S_BAR)} ` : ''; 36 - const errorPrefixEnd = hasGuide ? color.yellow(S_BAR_END) : ''; 34 + const errorText = this.error ? ` ${styleText('yellow', this.error)}` : ''; 35 + const errorPrefix = hasGuide ? `${styleText('yellow', S_BAR)} ` : ''; 36 + const errorPrefixEnd = hasGuide ? styleText('yellow', S_BAR_END) : ''; 37 37 return `${title.trim()}\n${errorPrefix}${userInput}\n${errorPrefixEnd}${errorText}\n`; 38 38 } 39 39 case 'submit': { 40 - const valueText = value ? ` ${color.dim(value)}` : ''; 41 - const submitPrefix = hasGuide ? color.gray(S_BAR) : ''; 40 + const valueText = value ? ` ${styleText('dim', value)}` : ''; 41 + const submitPrefix = hasGuide ? styleText('gray', S_BAR) : ''; 42 42 return `${title}${submitPrefix}${valueText}`; 43 43 } 44 44 case 'cancel': { 45 - const valueText = value ? ` ${color.strikethrough(color.dim(value))}` : ''; 46 - const cancelPrefix = hasGuide ? color.gray(S_BAR) : ''; 45 + const valueText = value ? ` ${styleText(['strikethrough', 'dim'], value)}` : ''; 46 + const cancelPrefix = hasGuide ? styleText('gray', S_BAR) : ''; 47 47 return `${title}${cancelPrefix}${valueText}${value.trim() ? `\n${cancelPrefix}` : ''}`; 48 48 } 49 49 default: { 50 - const defaultPrefix = hasGuide ? `${color.cyan(S_BAR)} ` : ''; 51 - const defaultPrefixEnd = hasGuide ? color.cyan(S_BAR_END) : ''; 50 + const defaultPrefix = hasGuide ? `${styleText('cyan', S_BAR)} ` : ''; 51 + const defaultPrefixEnd = hasGuide ? styleText('cyan', S_BAR_END) : ''; 52 52 return `${title}${defaultPrefix}${userInput}\n${defaultPrefixEnd}\n`; 53 53 } 54 54 }
+256 -33
packages/prompts/test/__snapshots__/autocomplete.test.ts.snap
··· 12 12 │ ○ Cherry 13 13 │ ○ Grape 14 14 │ ○ Orange 15 - │ ↑/↓ to select • Enter: confirm • Type: to search 15 + │ ↑/↓ to select • Enter: confirm • Type: to search 16 + └", 17 + " 18 + ", 19 + "<cursor.show>", 20 + ] 21 + `; 22 + 23 + exports[`autocomplete > cannot select disabled options when only one left 1`] = ` 24 + [ 25 + "<cursor.hide>", 26 + "│ 27 + ◆ Select a fruit 28 + │ 29 + │ Search: _ 30 + │ ● Apple 31 + │ ○ Banana 32 + │ ○ Cherry 33 + │ ○ Grape 34 + │ ○ Orange 35 + │ ○ Kiwi 36 + │ ↑/↓ to select • Enter: confirm • Type: to search 37 + └", 38 + "<cursor.backward count=999><cursor.up count=11>", 39 + "<cursor.down count=3>", 40 + "<erase.down>", 41 + "│ Search: k█ (1 match) 42 + │ ○ Kiwi 43 + │ ↑/↓ to select • Enter: confirm • Type: to search 44 + └", 45 + "<cursor.backward count=999><cursor.up count=6>", 46 + "<cursor.down count=1>", 47 + "<erase.down>", 48 + "◇ Select a fruit 49 + │", 50 + " 51 + ", 52 + "<cursor.show>", 53 + ] 54 + `; 55 + 56 + exports[`autocomplete > displays disabled options correctly 1`] = ` 57 + [ 58 + "<cursor.hide>", 59 + "│ 60 + ◆ Select a fruit 61 + │ 62 + │ Search: _ 63 + │ ● Apple 64 + │ ○ Banana 65 + │ ○ Cherry 66 + │ ○ Grape 67 + │ ○ Orange 68 + │ ○ Kiwi 69 + │ ↑/↓ to select • Enter: confirm • Type: to search 70 + └", 71 + "<cursor.backward count=999><cursor.up count=11>", 72 + "<cursor.down count=3>", 73 + "<erase.down>", 74 + "│ Search: 75 + │ ○ Apple 76 + │ ● Banana 77 + │ ○ Cherry 78 + │ ○ Grape 79 + │ ○ Orange 80 + │ ○ Kiwi 81 + │ ↑/↓ to select • Enter: confirm • Type: to search 82 + └", 83 + "<cursor.backward count=999><cursor.up count=11>", 84 + "<cursor.down count=5>", 85 + "<erase.down>", 86 + "│ ○ Banana 87 + │ ● Cherry 88 + │ ○ Grape 89 + │ ○ Orange 90 + │ ○ Kiwi 91 + │ ↑/↓ to select • Enter: confirm • Type: to search 16 92 └", 93 + "<cursor.backward count=999><cursor.up count=11>", 94 + "<cursor.down count=6>", 95 + "<erase.down>", 96 + "│ ○ Cherry 97 + │ ● Grape 98 + │ ○ Orange 99 + │ ○ Kiwi 100 + │ ↑/↓ to select • Enter: confirm • Type: to search 101 + └", 102 + "<cursor.backward count=999><cursor.up count=11>", 103 + "<cursor.down count=7>", 104 + "<erase.down>", 105 + "│ ○ Grape 106 + │ ● Orange 107 + │ ○ Kiwi 108 + │ ↑/↓ to select • Enter: confirm • Type: to search 109 + └", 110 + "<cursor.backward count=999><cursor.up count=11>", 111 + "<cursor.down count=4>", 112 + "<erase.down>", 113 + "│ ● Apple 114 + │ ○ Banana 115 + │ ○ Cherry 116 + │ ○ Grape 117 + │ ○ Orange 118 + │ ○ Kiwi 119 + │ ↑/↓ to select • Enter: confirm • Type: to search 120 + └", 121 + "<cursor.backward count=999><cursor.up count=11>", 122 + "<cursor.down count=1>", 123 + "<erase.down>", 124 + "◇ Select a fruit 125 + │ Apple", 17 126 " 18 127 ", 19 128 "<cursor.show>", ··· 33 142 │ ○ Option 3 34 143 │ ○ Option 4 35 144 │ ... 36 - │ ↑/↓ to select • Enter: confirm • Type: to search 145 + │ ↑/↓ to select • Enter: confirm • Type: to search 37 146 └", 38 147 "<cursor.backward count=999><cursor.up count=11>", 39 148 "<cursor.down count=1>", ··· 58 167 │ ○ Cherry 59 168 │ ○ Grape 60 169 │ ○ Orange 61 - │ ↑/↓ to select • Enter: confirm • Type: to search 170 + │ ↑/↓ to select • Enter: confirm • Type: to search 62 171 └", 63 172 "<cursor.backward count=999><cursor.up count=10>", 64 173 "<cursor.down count=3>", ··· 66 175 "│ Search: g█ (2 matches) 67 176 │ ● Grape 68 177 │ ○ Orange 69 - │ ↑/↓ to select • Enter: confirm • Type: to search 178 + │ ↑/↓ to select • Enter: confirm • Type: to search 70 179 └", 71 180 "<cursor.backward count=999><cursor.up count=7>", 72 181 "<cursor.down count=1>", ··· 91 200 │ Line 2 92 201 │ Line 3 93 202 │ ... 94 - │ ↑/↓ to select • Enter: confirm • Type: to search 203 + │ ↑/↓ to select • Enter: confirm • Type: to search 95 204 └", 96 205 "<cursor.backward count=999><cursor.up count=10>", 97 206 "<erase.down>", ··· 118 227 │ ○ Cherry 119 228 │ ○ Grape 120 229 │ ○ Orange 121 - │ ↑/↓ to select • Enter: confirm • Type: to search 230 + │ ↑/↓ to select • Enter: confirm • Type: to search 122 231 └", 123 232 "<cursor.backward count=999><cursor.up count=10>", 124 233 "<cursor.down count=1>", ··· 143 252 │ ○ Cherry 144 253 │ ○ Grape 145 254 │ ○ Orange 146 - │ ↑/↓ to select • Enter: confirm • Type: to search 255 + │ ↑/↓ to select • Enter: confirm • Type: to search 147 256 └", 148 257 "<cursor.backward count=999><cursor.up count=10>", 149 258 "<cursor.down count=1>", ··· 165 274 │ Search: _ 166 275 │ ... 167 276 │ ● Option 2 168 - │ ↑/↓ to select • Enter: confirm • Type: to search 277 + │ ↑/↓ to select • Enter: confirm • Type: to search 169 278 └", 170 279 "<cursor.backward count=999><cursor.up count=7>", 171 280 "<erase.down>", ··· 191 300 │ ○ Grape 192 301 │ ○ Orange 193 302 │ ○ Kiwi 194 - │ ↑/↓ to select • Enter: confirm • Type: to search 303 + │ ↑/↓ to select • Enter: confirm • Type: to search 195 304 └", 196 305 "<cursor.backward count=999><cursor.up count=11>", 197 306 "<cursor.down count=3>", ··· 203 312 │ ○ Grape 204 313 │ ○ Orange 205 314 │ ○ Kiwi 206 - │ ↑/↓ to select • Enter: confirm • Type: to search 315 + │ ↑/↓ to select • Enter: confirm • Type: to search 207 316 └", 208 317 "<cursor.backward count=999><cursor.up count=11>", 209 318 "<cursor.down count=5>", ··· 213 322 │ ○ Grape 214 323 │ ○ Orange 215 324 │ ○ Kiwi 216 - │ ↑/↓ to select • Enter: confirm • Type: to search 325 + │ ↑/↓ to select • Enter: confirm • Type: to search 217 326 └", 218 327 "<cursor.backward count=999><cursor.up count=11>", 219 328 "<cursor.down count=6>", ··· 222 331 │ ● Grape 223 332 │ ○ Orange 224 333 │ ○ Kiwi 225 - │ ↑/↓ to select • Enter: confirm • Type: to search 334 + │ ↑/↓ to select • Enter: confirm • Type: to search 226 335 └", 227 336 "<cursor.backward count=999><cursor.up count=11>", 228 337 "<cursor.down count=7>", ··· 230 339 "│ ○ Grape 231 340 │ ● Orange 232 341 │ ○ Kiwi 233 - │ ↑/↓ to select • Enter: confirm • Type: to search 342 + │ ↑/↓ to select • Enter: confirm • Type: to search 234 343 └", 235 344 "<cursor.backward count=999><cursor.up count=11>", 236 345 "<cursor.down count=8>", 237 346 "<erase.down>", 238 347 "│ ○ Orange 239 348 │ ● Kiwi (New Zealand) 240 - │ ↑/↓ to select • Enter: confirm • Type: to search 349 + │ ↑/↓ to select • Enter: confirm • Type: to search 241 350 └", 242 351 "<cursor.backward count=999><cursor.up count=11>", 243 352 "<cursor.down count=1>", ··· 262 371 │ ○ Cherry 263 372 │ ○ Grape 264 373 │ ○ Orange 265 - │ ↑/↓ to select • Enter: confirm • Type: to search 374 + │ ↑/↓ to select • Enter: confirm • Type: to search 266 375 └", 267 376 "<cursor.backward count=999><cursor.up count=10>", 268 377 "<cursor.down count=3>", 269 378 "<erase.down>", 270 379 "│ Search: z█ (0 matches) 271 380 │ No matches found 272 - │ ↑/↓ to select • Enter: confirm • Type: to search 381 + │ ↑/↓ to select • Enter: confirm • Type: to search 273 382 └", 274 383 "<cursor.backward count=999><cursor.up count=6>", 275 384 "<cursor.down count=1>", ··· 294 403 │ ○ Cherry 295 404 │ ○ Grape 296 405 │ ○ Orange 297 - │ ↑/↓ to select • Enter: confirm • Type: to search 406 + │ ↑/↓ to select • Enter: confirm • Type: to search 298 407 └", 299 408 "<cursor.backward count=999><cursor.up count=10>", 300 409 "<cursor.down count=3>", ··· 305 414 │ ○ Cherry 306 415 │ ○ Grape 307 416 │ ○ Orange 308 - │ ↑/↓ to select • Enter: confirm • Type: to search 417 + │ ↑/↓ to select • Enter: confirm • Type: to search 309 418 └", 310 419 "<cursor.backward count=999><cursor.up count=10>", 311 420 "<cursor.down count=1>", ··· 330 439 │ ○ Cherry 331 440 │ ○ Grape 332 441 │ ○ Orange 333 - │ ↑/↓ to select • Enter: confirm • Type: to search 442 + │ ↑/↓ to select • Enter: confirm • Type: to search 334 443 └", 335 444 "<cursor.backward count=999><cursor.up count=10>", 336 445 "<cursor.down count=1>", ··· 355 464 │ ● Cherry 356 465 │ ○ Grape 357 466 │ ○ Orange 358 - │ ↑/↓ to select • Enter: confirm • Type: to search 467 + │ ↑/↓ to select • Enter: confirm • Type: to search 359 468 └", 360 469 "<cursor.backward count=999><cursor.up count=10>", 361 470 "<cursor.down count=1>", ··· 378 487 │ ● Apple 379 488 │ ○ Banana 380 489 │ ○ Cherry 381 - │ ↑/↓ to select • Enter: confirm • Type: to search 490 + │ ↑/↓ to select • Enter: confirm • Type: to search 382 491 └", 383 492 "<cursor.backward count=999><cursor.up count=8>", 384 493 "<cursor.down count=3>", ··· 386 495 "│ Search: a█ (2 matches) 387 496 │ ● Apple 388 497 │ ○ Banana 389 - │ ↑/↓ to select • Enter: confirm • Type: to search 498 + │ ↑/↓ to select • Enter: confirm • Type: to search 390 499 └", 391 500 "<cursor.backward count=999><cursor.up count=7>", 392 501 "<cursor.down count=1>", ··· 409 518 │ ● Apple 410 519 │ ○ Banana 411 520 │ ○ Cherry 412 - │ ↑/↓ to select • Enter: confirm • Type: to search 521 + │ ↑/↓ to select • Enter: confirm • Type: to search 413 522 └", 414 523 "<cursor.backward count=999><cursor.up count=8>", 415 524 "<cursor.down count=3>", 416 525 "<erase.down>", 417 526 "│ Search: a█ (1 match) 418 527 │ ● Apple 419 - │ ↑/↓ to select • Enter: confirm • Type: to search 528 + │ ↑/↓ to select • Enter: confirm • Type: to search 420 529 └", 421 530 "<cursor.backward count=999><cursor.up count=6>", 422 531 "<cursor.down count=1>", ··· 441 550 │ ◻ Cherry 442 551 │ ◻ Grape 443 552 │ ◻ Orange 444 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 553 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 445 554 └", 446 555 " 447 556 ", ··· 461 570 │ ◻ Cherry 462 571 │ ◻ Grape 463 572 │ ◻ Orange 464 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 573 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 465 574 └", 466 575 "<cursor.backward count=999><cursor.up count=10>", 467 576 "<cursor.down count=3>", ··· 472 581 │ ◻ Cherry 473 582 │ ◻ Grape 474 583 │ ◻ Orange 475 - │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 584 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 476 585 └", 477 586 "<cursor.backward count=999><cursor.up count=10>", 478 587 "<cursor.down count=5>", ··· 486 595 │ ◻ Cherry 487 596 │ ◻ Grape 488 597 │ ◻ Orange 489 - │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 598 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 490 599 └", 491 600 "<cursor.backward count=999><cursor.up count=10>", 492 601 "<cursor.down count=6>", ··· 504 613 ] 505 614 `; 506 615 616 + exports[`autocompleteMultiselect > cannot select disabled options when only one left 1`] = ` 617 + [ 618 + "<cursor.hide>", 619 + "│ 620 + ◆ Select a fruit 621 + │ 622 + │ Search: _ 623 + │ ◻ Apple 624 + │ ◻ Banana 625 + │ ◻ Cherry 626 + │ ◻ Grape 627 + │ ◻ Orange 628 + │ ◻ Kiwi 629 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 630 + └", 631 + "<cursor.backward count=999><cursor.up count=11>", 632 + "<cursor.down count=3>", 633 + "<erase.down>", 634 + "│ Search: k█ (1 match) 635 + │ ◻ Kiwi 636 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 637 + └", 638 + "<cursor.backward count=999><cursor.up count=6>", 639 + "<cursor.down count=1>", 640 + "<erase.down>", 641 + "◇ Select a fruit 642 + │ 0 items selected", 643 + " 644 + ", 645 + "<cursor.show>", 646 + ] 647 + `; 648 + 649 + exports[`autocompleteMultiselect > displays disabled options correctly 1`] = ` 650 + [ 651 + "<cursor.hide>", 652 + "│ 653 + ◆ Select a fruit 654 + │ 655 + │ Search: _ 656 + │ ◻ Apple 657 + │ ◻ Banana 658 + │ ◻ Cherry 659 + │ ◻ Grape 660 + │ ◻ Orange 661 + │ ◻ Kiwi 662 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 663 + └", 664 + "<cursor.backward count=999><cursor.up count=11>", 665 + "<cursor.down count=3>", 666 + "<erase.down>", 667 + "│ Search:  668 + │ ◻ Apple 669 + │ ◻ Banana 670 + │ ◻ Cherry 671 + │ ◻ Grape 672 + │ ◻ Orange 673 + │ ◻ Kiwi 674 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 675 + └", 676 + "<cursor.backward count=999><cursor.up count=11>", 677 + "<cursor.down count=5>", 678 + "<erase.down>", 679 + "│ ◻ Banana 680 + │ ◻ Cherry 681 + │ ◻ Grape 682 + │ ◻ Orange 683 + │ ◻ Kiwi 684 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 685 + └", 686 + "<cursor.backward count=999><cursor.up count=11>", 687 + "<cursor.down count=6>", 688 + "<erase.down>", 689 + "│ ◻ Cherry 690 + │ ◻ Grape 691 + │ ◻ Orange 692 + │ ◻ Kiwi 693 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 694 + └", 695 + "<cursor.backward count=999><cursor.up count=11>", 696 + "<cursor.down count=7>", 697 + "<erase.down>", 698 + "│ ◻ Grape 699 + │ ◻ Orange 700 + │ ◻ Kiwi 701 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 702 + └", 703 + "<cursor.backward count=999><cursor.up count=11>", 704 + "<cursor.down count=4>", 705 + "<erase.down>", 706 + "│ ◻ Apple 707 + │ ◻ Banana 708 + │ ◻ Cherry 709 + │ ◻ Grape 710 + │ ◻ Orange 711 + │ ◻ Kiwi 712 + │ ↑/↓ to navigate • Space/Tab: select • Enter: confirm • Type: to search 713 + └", 714 + "<cursor.backward count=999><cursor.up count=11>", 715 + "<cursor.down count=4>", 716 + "<erase.line><cursor.left count=1>", 717 + "│ ◼ Apple", 718 + "<cursor.down count=7>", 719 + "<cursor.backward count=999><cursor.up count=11>", 720 + "<cursor.down count=1>", 721 + "<erase.down>", 722 + "◇ Select a fruit 723 + │ 1 items selected", 724 + " 725 + ", 726 + "<cursor.show>", 727 + ] 728 + `; 729 + 507 730 exports[`autocompleteMultiselect > renders error when empty selection & required is true 1`] = ` 508 731 [ 509 732 "<cursor.hide>", ··· 516 739 │ ◻ Cherry 517 740 │ ◻ Grape 518 741 │ ◻ Orange 519 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 742 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 520 743 └", 521 744 "<cursor.backward count=999><cursor.up count=10>", 522 745 "<cursor.down count=1>", ··· 530 753 │ ◻ Cherry 531 754 │ ◻ Grape 532 755 │ ◻ Orange 533 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 756 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 534 757 └", 535 758 "<cursor.backward count=999><cursor.up count=11>", 536 759 "<cursor.down count=1>", ··· 543 766 │ ◻ Cherry 544 767 │ ◻ Grape 545 768 │ ◻ Orange 546 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 769 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 547 770 └", 548 771 "<cursor.backward count=999><cursor.up count=10>", 549 772 "<cursor.down count=1>", ··· 568 791 │ ◻ Cherry 569 792 │ ◻ Grape 570 793 │ ◻ Orange 571 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 794 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 572 795 └", 573 796 "<cursor.backward count=999><cursor.up count=10>", 574 797 "<cursor.down count=3>", 575 798 "<erase.down>", 576 799 "│ Search: a█ (1 match) 577 800 │ ◻ Apple 578 - │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 801 + │ ↑/↓ to navigate • Tab: select • Enter: confirm • Type: to search 579 802 └", 580 803 "<cursor.backward count=999><cursor.up count=6>", 581 804 "<cursor.down count=4>",
+108
packages/prompts/test/__snapshots__/confirm.test.ts.snap
··· 53 53 ] 54 54 `; 55 55 56 + exports[`confirm (isCI = false) > global withGuide: false removes guide 1`] = ` 57 + [ 58 + "<cursor.hide>", 59 + "◆ foo 60 + ● Yes / ○ No 61 + 62 + ", 63 + "<cursor.backward count=999><cursor.up count=3>", 64 + "<erase.down>", 65 + "◇ foo 66 + Yes", 67 + " 68 + ", 69 + "<cursor.show>", 70 + ] 71 + `; 72 + 56 73 exports[`confirm (isCI = false) > left arrow moves to previous choice 1`] = ` 57 74 [ 58 75 "<cursor.hide>", ··· 139 156 ] 140 157 `; 141 158 159 + exports[`confirm (isCI = false) > renders options in vertical alignment 1`] = ` 160 + [ 161 + "<cursor.hide>", 162 + "│ 163 + ◆ foo 164 + │ ● Yes 165 + │ ○ No 166 + └ 167 + ", 168 + "<cursor.backward count=999><cursor.up count=5>", 169 + "<cursor.down count=1>", 170 + "<erase.down>", 171 + "◇ foo 172 + │ Yes", 173 + " 174 + ", 175 + "<cursor.show>", 176 + ] 177 + `; 178 + 142 179 exports[`confirm (isCI = false) > right arrow moves to next choice 1`] = ` 143 180 [ 144 181 "<cursor.hide>", ··· 157 194 "<erase.down>", 158 195 "◇ foo 159 196 │ No", 197 + " 198 + ", 199 + "<cursor.show>", 200 + ] 201 + `; 202 + 203 + exports[`confirm (isCI = false) > withGuide: false removes guide 1`] = ` 204 + [ 205 + "<cursor.hide>", 206 + "◆ foo 207 + ● Yes / ○ No 208 + 209 + ", 210 + "<cursor.backward count=999><cursor.up count=3>", 211 + "<erase.down>", 212 + "◇ foo 213 + Yes", 160 214 " 161 215 ", 162 216 "<cursor.show>", ··· 216 270 ] 217 271 `; 218 272 273 + exports[`confirm (isCI = true) > global withGuide: false removes guide 1`] = ` 274 + [ 275 + "<cursor.hide>", 276 + "◆ foo 277 + ● Yes / ○ No 278 + 279 + ", 280 + "<cursor.backward count=999><cursor.up count=3>", 281 + "<erase.down>", 282 + "◇ foo 283 + Yes", 284 + " 285 + ", 286 + "<cursor.show>", 287 + ] 288 + `; 289 + 219 290 exports[`confirm (isCI = true) > left arrow moves to previous choice 1`] = ` 220 291 [ 221 292 "<cursor.hide>", ··· 302 373 ] 303 374 `; 304 375 376 + exports[`confirm (isCI = true) > renders options in vertical alignment 1`] = ` 377 + [ 378 + "<cursor.hide>", 379 + "│ 380 + ◆ foo 381 + │ ● Yes 382 + │ ○ No 383 + └ 384 + ", 385 + "<cursor.backward count=999><cursor.up count=5>", 386 + "<cursor.down count=1>", 387 + "<erase.down>", 388 + "◇ foo 389 + │ Yes", 390 + " 391 + ", 392 + "<cursor.show>", 393 + ] 394 + `; 395 + 305 396 exports[`confirm (isCI = true) > right arrow moves to next choice 1`] = ` 306 397 [ 307 398 "<cursor.hide>", ··· 325 416 "<cursor.show>", 326 417 ] 327 418 `; 419 + 420 + exports[`confirm (isCI = true) > withGuide: false removes guide 1`] = ` 421 + [ 422 + "<cursor.hide>", 423 + "◆ foo 424 + ● Yes / ○ No 425 + 426 + ", 427 + "<cursor.backward count=999><cursor.up count=3>", 428 + "<erase.down>", 429 + "◇ foo 430 + Yes", 431 + " 432 + ", 433 + "<cursor.show>", 434 + ] 435 + `;
+70 -2
packages/prompts/test/__snapshots__/password.test.ts.snap
··· 57 57 ] 58 58 `; 59 59 60 + exports[`password (isCI = false) > global withGuide: false removes guide 1`] = ` 61 + [ 62 + "<cursor.hide>", 63 + "◆ foo 64 + _ 65 + 66 + ", 67 + "<cursor.backward count=999><cursor.up count=3>", 68 + "<erase.down>", 69 + "◇ foo 70 + ", 71 + " 72 + ", 73 + "<cursor.show>", 74 + ] 75 + `; 76 + 60 77 exports[`password (isCI = false) > renders and clears validation errors 1`] = ` 61 78 [ 62 79 "<cursor.hide>", ··· 190 207 "<cursor.down count=1>", 191 208 "<erase.down>", 192 209 "◇ foo 193 - │", 210 + │ ", 211 + " 212 + ", 213 + "<cursor.show>", 214 + ] 215 + `; 216 + 217 + exports[`password (isCI = false) > withGuide: false removes guide 1`] = ` 218 + [ 219 + "<cursor.hide>", 220 + "◆ foo 221 + _ 222 + 223 + ", 224 + "<cursor.backward count=999><cursor.up count=3>", 225 + "<erase.down>", 226 + "◇ foo 227 + ", 194 228 " 195 229 ", 196 230 "<cursor.show>", ··· 254 288 ] 255 289 `; 256 290 291 + exports[`password (isCI = true) > global withGuide: false removes guide 1`] = ` 292 + [ 293 + "<cursor.hide>", 294 + "◆ foo 295 + _ 296 + 297 + ", 298 + "<cursor.backward count=999><cursor.up count=3>", 299 + "<erase.down>", 300 + "◇ foo 301 + ", 302 + " 303 + ", 304 + "<cursor.show>", 305 + ] 306 + `; 307 + 257 308 exports[`password (isCI = true) > renders and clears validation errors 1`] = ` 258 309 [ 259 310 "<cursor.hide>", ··· 387 438 "<cursor.down count=1>", 388 439 "<erase.down>", 389 440 "◇ foo 390 - │", 441 + │ ", 442 + " 443 + ", 444 + "<cursor.show>", 445 + ] 446 + `; 447 + 448 + exports[`password (isCI = true) > withGuide: false removes guide 1`] = ` 449 + [ 450 + "<cursor.hide>", 451 + "◆ foo 452 + _ 453 + 454 + ", 455 + "<cursor.backward count=999><cursor.up count=3>", 456 + "<erase.down>", 457 + "◇ foo 458 + ", 391 459 " 392 460 ", 393 461 "<cursor.show>",
+44 -44
packages/prompts/test/__snapshots__/path.test.ts.snap
··· 9 9 │ Search: /tmp/█ 10 10 │ ● /tmp/bar 11 11 │ ○ /tmp/root.zip 12 - │ ↑/↓ to select • Enter: confirm • Type: to search 12 + │ ↑/↓ to select • Enter: confirm • Type: to search 13 13 └", 14 14 "<cursor.backward count=999><cursor.up count=7>", 15 15 "<cursor.down count=1>", ··· 31 31 │ Search: /tmp/█ 32 32 │ ● /tmp/bar 33 33 │ ○ /tmp/root.zip 34 - │ ↑/↓ to select • Enter: confirm • Type: to search 34 + │ ↑/↓ to select • Enter: confirm • Type: to search 35 35 └", 36 36 "<cursor.backward count=999><cursor.up count=7>", 37 37 "<cursor.down count=3>", 38 38 "<erase.down>", 39 39 "│ Search: /tmp/_█ 40 40 │ No matches found 41 - │ ↑/↓ to select • Enter: confirm • Type: to search 41 + │ ↑/↓ to select • Enter: confirm • Type: to search 42 42 └", 43 43 "<cursor.backward count=999><cursor.up count=6>", 44 44 "<cursor.down count=1>", ··· 48 48 │ Search: /tmp/_█ 49 49 │ No matches found 50 50 │ Please select a path 51 - │ ↑/↓ to select • Enter: confirm • Type: to search 51 + │ ↑/↓ to select • Enter: confirm • Type: to search 52 52 └", 53 53 "<cursor.backward count=999><cursor.up count=7>", 54 54 "<cursor.down count=1>", ··· 58 58 │ Search: /tmp/█ 59 59 │ ● /tmp/bar 60 60 │ ○ /tmp/root.zip 61 - │ ↑/↓ to select • Enter: confirm • Type: to search 61 + │ ↑/↓ to select • Enter: confirm • Type: to search 62 62 └", 63 63 "<cursor.backward count=999><cursor.up count=7>", 64 64 "<cursor.down count=3>", 65 65 "<erase.down>", 66 66 "│ Search: /tmp/b█ 67 67 │ ● /tmp/bar 68 - │ ↑/↓ to select • Enter: confirm • Type: to search 68 + │ ↑/↓ to select • Enter: confirm • Type: to search 69 69 └", 70 70 "<cursor.backward count=999><cursor.up count=6>", 71 71 "<cursor.down count=1>", ··· 86 86 │ 87 87 │ Search: /tmp/bar█ 88 88 │ ● /tmp/bar 89 - │ ↑/↓ to select • Enter: confirm • Type: to search 89 + │ ↑/↓ to select • Enter: confirm • Type: to search 90 90 └", 91 91 "<cursor.backward count=999><cursor.up count=6>", 92 92 "<cursor.down count=1>", ··· 108 108 │ Search: /tmp/█ 109 109 │ ● /tmp/bar 110 110 │ ○ /tmp/root.zip 111 - │ ↑/↓ to select • Enter: confirm • Type: to search 111 + │ ↑/↓ to select • Enter: confirm • Type: to search 112 112 └", 113 113 "<cursor.backward count=999><cursor.up count=7>", 114 114 "<cursor.down count=3>", 115 115 "<erase.down>", 116 116 "│ Search: /tmp/x█ 117 117 │ No matches found 118 - │ ↑/↓ to select • Enter: confirm • Type: to search 118 + │ ↑/↓ to select • Enter: confirm • Type: to search 119 119 └", 120 120 "<cursor.backward count=999><cursor.up count=6>", 121 121 "<cursor.down count=3>", ··· 142 142 │ Search: /tmp/█ 143 143 │ ● /tmp/bar 144 144 │ ○ /tmp/root.zip 145 - │ ↑/↓ to select • Enter: confirm • Type: to search 145 + │ ↑/↓ to select • Enter: confirm • Type: to search 146 146 └", 147 147 "<cursor.backward count=999><cursor.up count=7>", 148 148 "<cursor.down count=1>", ··· 164 164 │ Search: /tmp/█ 165 165 │ ● /tmp/bar 166 166 │ ○ /tmp/root.zip 167 - │ ↑/↓ to select • Enter: confirm • Type: to search 167 + │ ↑/↓ to select • Enter: confirm • Type: to search 168 168 └", 169 169 "<cursor.backward count=999><cursor.up count=7>", 170 170 "<cursor.down count=3>", 171 171 "<erase.down>", 172 172 "│ Search: /tmp/b█ 173 173 │ ● /tmp/bar 174 - │ ↑/↓ to select • Enter: confirm • Type: to search 174 + │ ↑/↓ to select • Enter: confirm • Type: to search 175 175 └", 176 176 "<cursor.backward count=999><cursor.up count=6>", 177 177 "<cursor.down count=3>", ··· 198 198 │ Search: /tmp/█ 199 199 │ ● /tmp/bar 200 200 │ ○ /tmp/root.zip 201 - │ ↑/↓ to select • Enter: confirm • Type: to search 201 + │ ↑/↓ to select • Enter: confirm • Type: to search 202 202 └", 203 203 "<cursor.backward count=999><cursor.up count=7>", 204 204 "<cursor.down count=3>", 205 205 "<erase.down>", 206 206 "│ Search: /tmp/r█ 207 207 │ ● /tmp/root.zip 208 - │ ↑/↓ to select • Enter: confirm • Type: to search 208 + │ ↑/↓ to select • Enter: confirm • Type: to search 209 209 └", 210 210 "<cursor.backward count=999><cursor.up count=6>", 211 211 "<cursor.down count=1>", ··· 215 215 │ Search: /tmp/r█ 216 216 │ should be /tmp/bar 217 217 │ ● /tmp/root.zip 218 - │ ↑/↓ to select • Enter: confirm • Type: to search 218 + │ ↑/↓ to select • Enter: confirm • Type: to search 219 219 └", 220 220 "<cursor.backward count=999><cursor.up count=7>", 221 221 "<cursor.down count=1>", ··· 225 225 │ Search: /tmp/█ 226 226 │ ○ /tmp/bar 227 227 │ ● /tmp/root.zip 228 - │ ↑/↓ to select • Enter: confirm • Type: to search 228 + │ ↑/↓ to select • Enter: confirm • Type: to search 229 229 └", 230 230 "<cursor.backward count=999><cursor.up count=7>", 231 231 "<cursor.down count=3>", 232 232 "<erase.down>", 233 233 "│ Search: /tmp/b█ 234 234 │ ● /tmp/bar 235 - │ ↑/↓ to select • Enter: confirm • Type: to search 235 + │ ↑/↓ to select • Enter: confirm • Type: to search 236 236 └", 237 237 "<cursor.backward count=999><cursor.up count=6>", 238 238 "<cursor.down count=1>", ··· 254 254 │ Search: /tmp/█ 255 255 │ ● /tmp/bar 256 256 │ ○ /tmp/root.zip 257 - │ ↑/↓ to select • Enter: confirm • Type: to search 257 + │ ↑/↓ to select • Enter: confirm • Type: to search 258 258 └", 259 259 "<cursor.backward count=999><cursor.up count=7>", 260 260 "<cursor.down count=3>", 261 261 "<erase.down>", 262 262 "│ Search: /tmp/r█ 263 263 │ ● /tmp/root.zip 264 - │ ↑/↓ to select • Enter: confirm • Type: to search 264 + │ ↑/↓ to select • Enter: confirm • Type: to search 265 265 └", 266 266 "<cursor.backward count=999><cursor.up count=6>", 267 267 "<cursor.down count=1>", ··· 271 271 │ Search: /tmp/r█ 272 272 │ should be /tmp/bar 273 273 │ ● /tmp/root.zip 274 - │ ↑/↓ to select • Enter: confirm • Type: to search 274 + │ ↑/↓ to select • Enter: confirm • Type: to search 275 275 └", 276 276 "<cursor.backward count=999><cursor.up count=7>", 277 277 "<cursor.down count=1>", ··· 281 281 │ Search: /tmp/█ 282 282 │ ○ /tmp/bar 283 283 │ ● /tmp/root.zip 284 - │ ↑/↓ to select • Enter: confirm • Type: to search 284 + │ ↑/↓ to select • Enter: confirm • Type: to search 285 285 └", 286 286 "<cursor.backward count=999><cursor.up count=7>", 287 287 "<cursor.down count=3>", 288 288 "<erase.down>", 289 289 "│ Search: /tmp/b█ 290 290 │ ● /tmp/bar 291 - │ ↑/↓ to select • Enter: confirm • Type: to search 291 + │ ↑/↓ to select • Enter: confirm • Type: to search 292 292 └", 293 293 "<cursor.backward count=999><cursor.up count=6>", 294 294 "<cursor.down count=1>", ··· 310 310 │ Search: /tmp/█ 311 311 │ ● /tmp/bar 312 312 │ ○ /tmp/root.zip 313 - │ ↑/↓ to select • Enter: confirm • Type: to search 313 + │ ↑/↓ to select • Enter: confirm • Type: to search 314 314 └", 315 315 "<cursor.backward count=999><cursor.up count=7>", 316 316 "<cursor.down count=1>", ··· 332 332 │ Search: /tmp/█ 333 333 │ ● /tmp/bar 334 334 │ ○ /tmp/root.zip 335 - │ ↑/↓ to select • Enter: confirm • Type: to search 335 + │ ↑/↓ to select • Enter: confirm • Type: to search 336 336 └", 337 337 "<cursor.backward count=999><cursor.up count=7>", 338 338 "<cursor.down count=3>", 339 339 "<erase.down>", 340 340 "│ Search: /tmp/_█ 341 341 │ No matches found 342 - │ ↑/↓ to select • Enter: confirm • Type: to search 342 + │ ↑/↓ to select • Enter: confirm • Type: to search 343 343 └", 344 344 "<cursor.backward count=999><cursor.up count=6>", 345 345 "<cursor.down count=1>", ··· 349 349 │ Search: /tmp/_█ 350 350 │ No matches found 351 351 │ Please select a path 352 - │ ↑/↓ to select • Enter: confirm • Type: to search 352 + │ ↑/↓ to select • Enter: confirm • Type: to search 353 353 └", 354 354 "<cursor.backward count=999><cursor.up count=7>", 355 355 "<cursor.down count=1>", ··· 359 359 │ Search: /tmp/█ 360 360 │ ● /tmp/bar 361 361 │ ○ /tmp/root.zip 362 - │ ↑/↓ to select • Enter: confirm • Type: to search 362 + │ ↑/↓ to select • Enter: confirm • Type: to search 363 363 └", 364 364 "<cursor.backward count=999><cursor.up count=7>", 365 365 "<cursor.down count=3>", 366 366 "<erase.down>", 367 367 "│ Search: /tmp/b█ 368 368 │ ● /tmp/bar 369 - │ ↑/↓ to select • Enter: confirm • Type: to search 369 + │ ↑/↓ to select • Enter: confirm • Type: to search 370 370 └", 371 371 "<cursor.backward count=999><cursor.up count=6>", 372 372 "<cursor.down count=1>", ··· 387 387 │ 388 388 │ Search: /tmp/bar█ 389 389 │ ● /tmp/bar 390 - │ ↑/↓ to select • Enter: confirm • Type: to search 390 + │ ↑/↓ to select • Enter: confirm • Type: to search 391 391 └", 392 392 "<cursor.backward count=999><cursor.up count=6>", 393 393 "<cursor.down count=1>", ··· 409 409 │ Search: /tmp/█ 410 410 │ ● /tmp/bar 411 411 │ ○ /tmp/root.zip 412 - │ ↑/↓ to select • Enter: confirm • Type: to search 412 + │ ↑/↓ to select • Enter: confirm • Type: to search 413 413 └", 414 414 "<cursor.backward count=999><cursor.up count=7>", 415 415 "<cursor.down count=3>", 416 416 "<erase.down>", 417 417 "│ Search: /tmp/x█ 418 418 │ No matches found 419 - │ ↑/↓ to select • Enter: confirm • Type: to search 419 + │ ↑/↓ to select • Enter: confirm • Type: to search 420 420 └", 421 421 "<cursor.backward count=999><cursor.up count=6>", 422 422 "<cursor.down count=3>", ··· 443 443 │ Search: /tmp/█ 444 444 │ ● /tmp/bar 445 445 │ ○ /tmp/root.zip 446 - │ ↑/↓ to select • Enter: confirm • Type: to search 446 + │ ↑/↓ to select • Enter: confirm • Type: to search 447 447 └", 448 448 "<cursor.backward count=999><cursor.up count=7>", 449 449 "<cursor.down count=1>", ··· 465 465 │ Search: /tmp/█ 466 466 │ ● /tmp/bar 467 467 │ ○ /tmp/root.zip 468 - │ ↑/↓ to select • Enter: confirm • Type: to search 468 + │ ↑/↓ to select • Enter: confirm • Type: to search 469 469 └", 470 470 "<cursor.backward count=999><cursor.up count=7>", 471 471 "<cursor.down count=3>", 472 472 "<erase.down>", 473 473 "│ Search: /tmp/b█ 474 474 │ ● /tmp/bar 475 - │ ↑/↓ to select • Enter: confirm • Type: to search 475 + │ ↑/↓ to select • Enter: confirm • Type: to search 476 476 └", 477 477 "<cursor.backward count=999><cursor.up count=6>", 478 478 "<cursor.down count=3>", ··· 499 499 │ Search: /tmp/█ 500 500 │ ● /tmp/bar 501 501 │ ○ /tmp/root.zip 502 - │ ↑/↓ to select • Enter: confirm • Type: to search 502 + │ ↑/↓ to select • Enter: confirm • Type: to search 503 503 └", 504 504 "<cursor.backward count=999><cursor.up count=7>", 505 505 "<cursor.down count=3>", 506 506 "<erase.down>", 507 507 "│ Search: /tmp/r█ 508 508 │ ● /tmp/root.zip 509 - │ ↑/↓ to select • Enter: confirm • Type: to search 509 + │ ↑/↓ to select • Enter: confirm • Type: to search 510 510 └", 511 511 "<cursor.backward count=999><cursor.up count=6>", 512 512 "<cursor.down count=1>", ··· 516 516 │ Search: /tmp/r█ 517 517 │ should be /tmp/bar 518 518 │ ● /tmp/root.zip 519 - │ ↑/↓ to select • Enter: confirm • Type: to search 519 + │ ↑/↓ to select • Enter: confirm • Type: to search 520 520 └", 521 521 "<cursor.backward count=999><cursor.up count=7>", 522 522 "<cursor.down count=1>", ··· 526 526 │ Search: /tmp/█ 527 527 │ ○ /tmp/bar 528 528 │ ● /tmp/root.zip 529 - │ ↑/↓ to select • Enter: confirm • Type: to search 529 + │ ↑/↓ to select • Enter: confirm • Type: to search 530 530 └", 531 531 "<cursor.backward count=999><cursor.up count=7>", 532 532 "<cursor.down count=3>", 533 533 "<erase.down>", 534 534 "│ Search: /tmp/b█ 535 535 │ ● /tmp/bar 536 - │ ↑/↓ to select • Enter: confirm • Type: to search 536 + │ ↑/↓ to select • Enter: confirm • Type: to search 537 537 └", 538 538 "<cursor.backward count=999><cursor.up count=6>", 539 539 "<cursor.down count=1>", ··· 555 555 │ Search: /tmp/█ 556 556 │ ● /tmp/bar 557 557 │ ○ /tmp/root.zip 558 - │ ↑/↓ to select • Enter: confirm • Type: to search 558 + │ ↑/↓ to select • Enter: confirm • Type: to search 559 559 └", 560 560 "<cursor.backward count=999><cursor.up count=7>", 561 561 "<cursor.down count=3>", 562 562 "<erase.down>", 563 563 "│ Search: /tmp/r█ 564 564 │ ● /tmp/root.zip 565 - │ ↑/↓ to select • Enter: confirm • Type: to search 565 + │ ↑/↓ to select • Enter: confirm • Type: to search 566 566 └", 567 567 "<cursor.backward count=999><cursor.up count=6>", 568 568 "<cursor.down count=1>", ··· 572 572 │ Search: /tmp/r█ 573 573 │ should be /tmp/bar 574 574 │ ● /tmp/root.zip 575 - │ ↑/↓ to select • Enter: confirm • Type: to search 575 + │ ↑/↓ to select • Enter: confirm • Type: to search 576 576 └", 577 577 "<cursor.backward count=999><cursor.up count=7>", 578 578 "<cursor.down count=1>", ··· 582 582 │ Search: /tmp/█ 583 583 │ ○ /tmp/bar 584 584 │ ● /tmp/root.zip 585 - │ ↑/↓ to select • Enter: confirm • Type: to search 585 + │ ↑/↓ to select • Enter: confirm • Type: to search 586 586 └", 587 587 "<cursor.backward count=999><cursor.up count=7>", 588 588 "<cursor.down count=3>", 589 589 "<erase.down>", 590 590 "│ Search: /tmp/b█ 591 591 │ ● /tmp/bar 592 - │ ↑/↓ to select • Enter: confirm • Type: to search 592 + │ ↑/↓ to select • Enter: confirm • Type: to search 593 593 └", 594 594 "<cursor.backward count=999><cursor.up count=6>", 595 595 "<cursor.down count=1>",
+72
packages/prompts/test/__snapshots__/select-key.test.ts.snap
··· 63 63 ] 64 64 `; 65 65 66 + exports[`text (isCI = false) > global withGuide: false removes guide 1`] = ` 67 + [ 68 + "<cursor.hide>", 69 + "◆ foo 70 +  a  Option A 71 +  b  Option B 72 + 73 + ", 74 + "<cursor.show>", 75 + "<cursor.backward count=999><cursor.up count=4>", 76 + "<erase.down>", 77 + "◇ foo 78 + Option A", 79 + " 80 + ", 81 + ] 82 + `; 83 + 66 84 exports[`text (isCI = false) > input is case-insensitive by default 1`] = ` 67 85 [ 68 86 "<cursor.hide>", ··· 265 283 "<erase.down>", 266 284 "◇ foo 267 285 │ Option B", 286 + " 287 + ", 288 + ] 289 + `; 290 + 291 + exports[`text (isCI = false) > withGuide: false removes guide 1`] = ` 292 + [ 293 + "<cursor.hide>", 294 + "◆ foo 295 +  a  Option A 296 +  b  Option B 297 + 298 + ", 299 + "<cursor.show>", 300 + "<cursor.backward count=999><cursor.up count=4>", 301 + "<erase.down>", 302 + "◇ foo 303 + Option A", 268 304 " 269 305 ", 270 306 ] ··· 333 369 ] 334 370 `; 335 371 372 + exports[`text (isCI = true) > global withGuide: false removes guide 1`] = ` 373 + [ 374 + "<cursor.hide>", 375 + "◆ foo 376 +  a  Option A 377 +  b  Option B 378 + 379 + ", 380 + "<cursor.show>", 381 + "<cursor.backward count=999><cursor.up count=4>", 382 + "<erase.down>", 383 + "◇ foo 384 + Option A", 385 + " 386 + ", 387 + ] 388 + `; 389 + 336 390 exports[`text (isCI = true) > input is case-insensitive by default 1`] = ` 337 391 [ 338 392 "<cursor.hide>", ··· 539 593 ", 540 594 ] 541 595 `; 596 + 597 + exports[`text (isCI = true) > withGuide: false removes guide 1`] = ` 598 + [ 599 + "<cursor.hide>", 600 + "◆ foo 601 +  a  Option A 602 +  b  Option B 603 + 604 + ", 605 + "<cursor.show>", 606 + "<cursor.backward count=999><cursor.up count=4>", 607 + "<erase.down>", 608 + "◇ foo 609 + Option A", 610 + " 611 + ", 612 + ] 613 + `;
+72
packages/prompts/test/__snapshots__/select.test.ts.snap
··· 181 181 ] 182 182 `; 183 183 184 + exports[`select (isCI = false) > global withGuide: false removes guide 1`] = ` 185 + [ 186 + "<cursor.hide>", 187 + "◆ foo 188 + ● opt0 189 + ○ opt1 190 + 191 + ", 192 + "<cursor.backward count=999><cursor.up count=4>", 193 + "<erase.down>", 194 + "◇ foo 195 + opt0", 196 + " 197 + ", 198 + "<cursor.show>", 199 + ] 200 + `; 201 + 184 202 exports[`select (isCI = false) > handles mixed size re-renders 1`] = ` 185 203 [ 186 204 "<cursor.hide>", ··· 357 375 "<erase.down>", 358 376 "◇ foo 359 377 │ opt0", 378 + " 379 + ", 380 + "<cursor.show>", 381 + ] 382 + `; 383 + 384 + exports[`select (isCI = false) > withGuide: false removes guide 1`] = ` 385 + [ 386 + "<cursor.hide>", 387 + "◆ foo 388 + ● opt0 389 + ○ opt1 390 + 391 + ", 392 + "<cursor.backward count=999><cursor.up count=4>", 393 + "<erase.down>", 394 + "◇ foo 395 + opt0", 360 396 " 361 397 ", 362 398 "<cursor.show>", ··· 625 661 ] 626 662 `; 627 663 664 + exports[`select (isCI = true) > global withGuide: false removes guide 1`] = ` 665 + [ 666 + "<cursor.hide>", 667 + "◆ foo 668 + ● opt0 669 + ○ opt1 670 + 671 + ", 672 + "<cursor.backward count=999><cursor.up count=4>", 673 + "<erase.down>", 674 + "◇ foo 675 + opt0", 676 + " 677 + ", 678 + "<cursor.show>", 679 + ] 680 + `; 681 + 628 682 exports[`select (isCI = true) > handles mixed size re-renders 1`] = ` 629 683 [ 630 684 "<cursor.hide>", ··· 801 855 "<erase.down>", 802 856 "◇ foo 803 857 │ opt0", 858 + " 859 + ", 860 + "<cursor.show>", 861 + ] 862 + `; 863 + 864 + exports[`select (isCI = true) > withGuide: false removes guide 1`] = ` 865 + [ 866 + "<cursor.hide>", 867 + "◆ foo 868 + ● opt0 869 + ○ opt1 870 + 871 + ", 872 + "<cursor.backward count=999><cursor.up count=4>", 873 + "<erase.down>", 874 + "◇ foo 875 + opt0", 804 876 " 805 877 ", 806 878 "<cursor.show>",
+52
packages/prompts/test/__snapshots__/spinner.test.ts.snap
··· 23 23 ] 24 24 `; 25 25 26 + exports[`spinner (isCI = false) > global withGuide: false removes guide 1`] = ` 27 + [ 28 + "<cursor.hide>", 29 + "◒ foo", 30 + "<cursor.left count=1>", 31 + "<erase.down>", 32 + "◇ 33 + ", 34 + "<cursor.show>", 35 + ] 36 + `; 37 + 26 38 exports[`spinner (isCI = false) > indicator customization > custom delay 1`] = ` 27 39 [ 28 40 "<cursor.hide>", ··· 571 583 ] 572 584 `; 573 585 586 + exports[`spinner (isCI = false) > withGuide: false removes guide 1`] = ` 587 + [ 588 + "<cursor.hide>", 589 + "◒ foo", 590 + "<cursor.left count=1>", 591 + "<erase.down>", 592 + "◇ 593 + ", 594 + "<cursor.show>", 595 + ] 596 + `; 597 + 574 598 exports[`spinner (isCI = true) > can be aborted by a signal 1`] = ` 575 599 [ 576 600 "<cursor.hide>", ··· 592 616 ", 593 617 "<cursor.left count=1>", 594 618 "<erase.down>", 619 + "<cursor.show>", 620 + ] 621 + `; 622 + 623 + exports[`spinner (isCI = true) > global withGuide: false removes guide 1`] = ` 624 + [ 625 + "<cursor.hide>", 626 + "◒ foo...", 627 + " 628 + ", 629 + "<cursor.left count=1>", 630 + "<erase.down>", 631 + "◇ 632 + ", 595 633 "<cursor.show>", 596 634 ] 597 635 `; ··· 954 992 "<cursor.show>", 955 993 ] 956 994 `; 995 + 996 + exports[`spinner (isCI = true) > withGuide: false removes guide 1`] = ` 997 + [ 998 + "<cursor.hide>", 999 + "◒ foo...", 1000 + " 1001 + ", 1002 + "<cursor.left count=1>", 1003 + "<erase.down>", 1004 + "◇ 1005 + ", 1006 + "<cursor.show>", 1007 + ] 1008 + `;
+74
packages/prompts/test/autocomplete.test.ts
··· 241 241 expect(output.buffer).toMatchSnapshot(); 242 242 expect(value).toBe('grape'); 243 243 }); 244 + 245 + test('displays disabled options correctly', async () => { 246 + const optionsWithDisabled = [...testOptions, { value: 'kiwi', label: 'Kiwi', disabled: true }]; 247 + const result = autocomplete({ 248 + message: 'Select a fruit', 249 + options: optionsWithDisabled, 250 + input, 251 + output, 252 + }); 253 + 254 + for (let i = 0; i < 5; i++) { 255 + input.emit('keypress', '', { name: 'down' }); 256 + } 257 + input.emit('keypress', '', { name: 'return' }); 258 + 259 + const value = await result; 260 + expect(value).toBe('apple'); 261 + expect(output.buffer).toMatchSnapshot(); 262 + }); 263 + 264 + test('cannot select disabled options when only one left', async () => { 265 + const optionsWithDisabled = [...testOptions, { value: 'kiwi', label: 'Kiwi', disabled: true }]; 266 + const result = autocomplete({ 267 + message: 'Select a fruit', 268 + options: optionsWithDisabled, 269 + input, 270 + output, 271 + }); 272 + 273 + input.emit('keypress', 'k', { name: 'k' }); 274 + input.emit('keypress', '', { name: 'return' }); 275 + 276 + const value = await result; 277 + expect(value).toBe(undefined); 278 + expect(output.buffer).toMatchSnapshot(); 279 + }); 244 280 }); 245 281 246 282 describe('autocompleteMultiselect', () => { ··· 334 370 335 371 const value = await result; 336 372 expect(value).toEqual(['apple']); 373 + expect(output.buffer).toMatchSnapshot(); 374 + }); 375 + 376 + test('displays disabled options correctly', async () => { 377 + const optionsWithDisabled = [...testOptions, { value: 'kiwi', label: 'Kiwi', disabled: true }]; 378 + const result = autocompleteMultiselect({ 379 + message: 'Select a fruit', 380 + options: optionsWithDisabled, 381 + input, 382 + output, 383 + }); 384 + 385 + for (let i = 0; i < testOptions.length; i++) { 386 + input.emit('keypress', '', { name: 'down' }); 387 + } 388 + input.emit('keypress', '', { name: 'tab' }); 389 + input.emit('keypress', '', { name: 'return' }); 390 + 391 + const value = await result; 392 + expect(value).toEqual(['apple']); 393 + expect(output.buffer).toMatchSnapshot(); 394 + }); 395 + 396 + test('cannot select disabled options when only one left', async () => { 397 + const optionsWithDisabled = [...testOptions, { value: 'kiwi', label: 'Kiwi', disabled: true }]; 398 + const result = autocompleteMultiselect({ 399 + message: 'Select a fruit', 400 + options: optionsWithDisabled, 401 + input, 402 + output, 403 + }); 404 + 405 + input.emit('keypress', 'k', { name: 'k' }); 406 + input.emit('keypress', '', { name: 'tab' }); 407 + input.emit('keypress', '', { name: 'return' }); 408 + 409 + const value = await result; 410 + expect(value).toEqual([]); 337 411 expect(output.buffer).toMatchSnapshot(); 338 412 }); 339 413 });
+2 -2
packages/prompts/test/box.test.ts
··· 1 + import { styleText } from 'node:util'; 1 2 import { updateSettings } from '@clack/core'; 2 - import colors from 'picocolors'; 3 3 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 4 4 import * as prompts from '../src/index.js'; 5 5 import { MockReadable, MockWritable } from './test-utils.js'; ··· 243 243 input, 244 244 output, 245 245 width: 'auto', 246 - formatBorder: colors.red, 246 + formatBorder: (text: string) => styleText('red', text), 247 247 }); 248 248 249 249 expect(output.buffer).toMatchSnapshot();
+49
packages/prompts/test/confirm.test.ts
··· 1 + import { updateSettings } from '@clack/core'; 1 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 2 3 import * as prompts from '../src/index.js'; 3 4 import { MockReadable, MockWritable } from './test-utils.js'; ··· 23 24 24 25 afterEach(() => { 25 26 vi.restoreAllMocks(); 27 + updateSettings({ withGuide: true }); 26 28 }); 27 29 28 30 test('renders message with choices', async () => { ··· 60 62 const result = prompts.confirm({ 61 63 message: 'foo', 62 64 inactive: 'bleep', 65 + input, 66 + output, 67 + }); 68 + 69 + input.emit('keypress', '', { name: 'return' }); 70 + 71 + const value = await result; 72 + 73 + expect(value).toBe(true); 74 + expect(output.buffer).toMatchSnapshot(); 75 + }); 76 + 77 + test('renders options in vertical alignment', async () => { 78 + const result = prompts.confirm({ 79 + message: 'foo', 80 + vertical: true, 63 81 input, 64 82 output, 65 83 }); ··· 148 166 controller.abort(); 149 167 const value = await result; 150 168 expect(prompts.isCancel(value)).toBe(true); 169 + expect(output.buffer).toMatchSnapshot(); 170 + }); 171 + 172 + test('withGuide: false removes guide', async () => { 173 + const result = prompts.confirm({ 174 + message: 'foo', 175 + withGuide: false, 176 + input, 177 + output, 178 + }); 179 + 180 + input.emit('keypress', '', { name: 'return' }); 181 + 182 + await result; 183 + 184 + expect(output.buffer).toMatchSnapshot(); 185 + }); 186 + 187 + test('global withGuide: false removes guide', async () => { 188 + updateSettings({ withGuide: false }); 189 + 190 + const result = prompts.confirm({ 191 + message: 'foo', 192 + input, 193 + output, 194 + }); 195 + 196 + input.emit('keypress', '', { name: 'return' }); 197 + 198 + await result; 199 + 151 200 expect(output.buffer).toMatchSnapshot(); 152 201 }); 153 202 });
+25 -12
packages/prompts/test/limit-options.test.ts
··· 1 - import color from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import { beforeEach, describe, expect, test } from 'vitest'; 3 3 import { type LimitOptionsParams, limitOptions } from '../src/index.js'; 4 4 import { MockWritable } from './test-utils.js'; ··· 39 39 ]; 40 40 options.maxItems = 3; 41 41 const result = limitOptions(options); 42 - expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', color.dim('...')]); 42 + expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', styleText('dim', '...')]); 43 43 }); 44 44 45 45 test('returns sliding window when cursor moves down', async () => { ··· 59 59 options.maxItems = 5; 60 60 options.cursor = 6; 61 61 const result = limitOptions(options); 62 - expect(result).toEqual([color.dim('...'), 'Item 6', 'Item 7', 'Item 8', color.dim('...')]); 62 + expect(result).toEqual([ 63 + styleText('dim', '...'), 64 + 'Item 6', 65 + 'Item 7', 66 + 'Item 8', 67 + styleText('dim', '...'), 68 + ]); 63 69 }); 64 70 65 71 test('returns sliding window near end of list', async () => { ··· 78 84 options.maxItems = 5; 79 85 options.cursor = 8; 80 86 const result = limitOptions(options); 81 - expect(result).toEqual([color.dim('...'), 'Item 7', 'Item 8', 'Item 9', 'Item 10']); 87 + expect(result).toEqual([styleText('dim', '...'), 'Item 7', 'Item 8', 'Item 9', 'Item 10']); 82 88 }); 83 89 84 90 test('handles empty options list', async () => { ··· 103 109 output.rows = 7; 104 110 options.maxItems = 10; 105 111 const result = limitOptions(options); 106 - expect(result).toEqual(['Item 1', 'Item 2', color.dim('...')]); 112 + expect(result).toEqual(['Item 1', 'Item 2', styleText('dim', '...')]); 107 113 }); 108 114 109 115 test('handle multi-line item clamping (start)', async () => { ··· 138 144 'Item 6', 139 145 'Item 7', 140 146 'Item 8', 141 - color.dim('...'), 147 + styleText('dim', '...'), 142 148 ]); 143 149 }); 144 150 ··· 164 170 options.cursor = 7; 165 171 const result = limitOptions(options); 166 172 expect(result).toEqual([ 167 - color.dim('...'), 173 + styleText('dim', '...'), 168 174 'Item 2', 169 175 'Item 3', 170 176 'Item 4', ··· 175 181 'Item 6', 176 182 'Item 7', 177 183 'Item 8', 178 - color.dim('...'), 184 + styleText('dim', '...'), 179 185 ]); 180 186 }); 181 187 ··· 201 207 options.cursor = 9; 202 208 const result = limitOptions(options); 203 209 expect(result).toEqual([ 204 - color.dim('...'), 210 + styleText('dim', '...'), 205 211 'Item 4', 206 212 'Item 5', 207 213 'Item 6', ··· 259 265 options.rowPadding = 6; 260 266 // Available rows for options = 12 - 6 = 6 261 267 const result = limitOptions(options); 262 - expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', 'Item 5', color.dim('...')]); 268 + expect(result).toEqual([ 269 + 'Item 1', 270 + 'Item 2', 271 + 'Item 3', 272 + 'Item 4', 273 + 'Item 5', 274 + styleText('dim', '...'), 275 + ]); 263 276 }); 264 277 265 278 test('respects custom rowPadding when scrolling', async () => { ··· 283 296 // Available rows for options = 12 - 6 = 6 284 297 const result = limitOptions(options); 285 298 expect(result).toEqual([ 286 - color.dim('...'), 299 + styleText('dim', '...'), 287 300 'Item 4', 288 301 'Item 5', 289 302 'Item 6', 290 303 'Item 7', 291 - color.dim('...'), 304 + styleText('dim', '...'), 292 305 ]); 293 306 }); 294 307 });
+3 -3
packages/prompts/test/log.test.ts
··· 1 - import colors from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 3 3 import * as prompts from '../src/index.js'; 4 4 import { MockWritable } from './test-utils.js'; ··· 51 51 52 52 test('renders message with custom symbols and spacing', () => { 53 53 prompts.log.message('custom\nsymbols', { 54 - symbol: colors.red('>>'), 55 - secondarySymbol: colors.yellow('--'), 54 + symbol: styleText('red', '>>'), 55 + secondarySymbol: styleText('yellow', '--'), 56 56 output, 57 57 }); 58 58
+4 -4
packages/prompts/test/note.test.ts
··· 1 - import colors from 'picocolors'; 1 + import { styleText } from 'node:util'; 2 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 3 3 import * as prompts from '../src/index.js'; 4 4 import { MockReadable, MockWritable } from './test-utils.js'; ··· 56 56 57 57 test('formatter which adds colors works', () => { 58 58 prompts.note('line 0\nline 1\nline 2', 'title', { 59 - format: (line) => colors.red(line), 59 + format: (line) => styleText('red', line), 60 60 input, 61 61 output, 62 62 }); ··· 79 79 const message = `${'test string '.repeat(32)}\n`.repeat(4).trim(); 80 80 output.columns = 75; 81 81 prompts.note(message, 'title', { 82 - format: (line) => colors.red(`* ${colors.cyan(line)} *`), 82 + format: (line) => styleText('red', `* ${styleText('cyan', line)} *`), 83 83 input, 84 84 output, 85 85 }); ··· 102 102 const messages = ['이게 첫 번째 줄이에요', 'これは次の行です']; 103 103 output.columns = 10; 104 104 prompts.note(messages.join('\n'), '这是标题', { 105 - format: (line) => colors.red(`* ${colors.cyan(line)} *`), 105 + format: (line) => styleText('red', `* ${styleText('cyan', line)} *`), 106 106 input, 107 107 output, 108 108 });
+33
packages/prompts/test/password.test.ts
··· 1 + import { updateSettings } from '@clack/core'; 1 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 2 3 import * as prompts from '../src/index.js'; 3 4 import { MockReadable, MockWritable } from './test-utils.js'; ··· 23 24 24 25 afterEach(() => { 25 26 vi.restoreAllMocks(); 27 + updateSettings({ withGuide: true }); 26 28 }); 27 29 28 30 test('renders message', async () => { ··· 147 149 const value = await result; 148 150 149 151 expect(value).toBe('yz'); 152 + expect(output.buffer).toMatchSnapshot(); 153 + }); 154 + 155 + test('withGuide: false removes guide', async () => { 156 + const result = prompts.password({ 157 + message: 'foo', 158 + withGuide: false, 159 + input, 160 + output, 161 + }); 162 + 163 + input.emit('keypress', '', { name: 'return' }); 164 + 165 + await result; 166 + 167 + expect(output.buffer).toMatchSnapshot(); 168 + }); 169 + 170 + test('global withGuide: false removes guide', async () => { 171 + updateSettings({ withGuide: false }); 172 + 173 + const result = prompts.password({ 174 + message: 'foo', 175 + input, 176 + output, 177 + }); 178 + 179 + input.emit('keypress', '', { name: 'return' }); 180 + 181 + await result; 182 + 150 183 expect(output.buffer).toMatchSnapshot(); 151 184 }); 152 185 });
+39
packages/prompts/test/select-key.test.ts
··· 211 211 expect(output.buffer).toMatchSnapshot(); 212 212 }); 213 213 214 + test('withGuide: false removes guide', async () => { 215 + const result = prompts.selectKey({ 216 + message: 'foo', 217 + options: [ 218 + { label: 'Option A', value: 'a' }, 219 + { label: 'Option B', value: 'b' }, 220 + ], 221 + withGuide: false, 222 + input, 223 + output, 224 + }); 225 + 226 + input.emit('keypress', 'a', { name: 'a' }); 227 + 228 + await result; 229 + 230 + expect(output.buffer).toMatchSnapshot(); 231 + }); 232 + 233 + test('global withGuide: false removes guide', async () => { 234 + updateSettings({ withGuide: false }); 235 + 236 + const result = prompts.selectKey({ 237 + message: 'foo', 238 + options: [ 239 + { label: 'Option A', value: 'a' }, 240 + { label: 'Option B', value: 'b' }, 241 + ], 242 + input, 243 + output, 244 + }); 245 + 246 + input.emit('keypress', 'a', { name: 'a' }); 247 + 248 + await result; 249 + 250 + expect(output.buffer).toMatchSnapshot(); 251 + }); 252 + 214 253 test('long submitted labels are wrapped correctly', async () => { 215 254 output.columns = 40; 216 255
+35
packages/prompts/test/select.test.ts
··· 1 + import { updateSettings } from '@clack/core'; 1 2 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 2 3 import * as prompts from '../src/index.js'; 3 4 import { MockReadable, MockWritable } from './test-utils.js'; ··· 23 24 24 25 afterEach(() => { 25 26 vi.restoreAllMocks(); 27 + updateSettings({ withGuide: true }); 26 28 }); 27 29 28 30 test('renders options and message', async () => { ··· 302 304 const value = await result; 303 305 304 306 expect(value).toBe('opt4'); 307 + expect(output.buffer).toMatchSnapshot(); 308 + }); 309 + 310 + test('withGuide: false removes guide', async () => { 311 + const result = prompts.select({ 312 + message: 'foo', 313 + options: [{ value: 'opt0' }, { value: 'opt1' }], 314 + withGuide: false, 315 + input, 316 + output, 317 + }); 318 + 319 + input.emit('keypress', '', { name: 'return' }); 320 + 321 + await result; 322 + 323 + expect(output.buffer).toMatchSnapshot(); 324 + }); 325 + 326 + test('global withGuide: false removes guide', async () => { 327 + updateSettings({ withGuide: false }); 328 + 329 + const result = prompts.select({ 330 + message: 'foo', 331 + options: [{ value: 'opt0' }, { value: 'opt1' }], 332 + input, 333 + output, 334 + }); 335 + 336 + input.emit('keypress', '', { name: 'return' }); 337 + 338 + await result; 339 + 305 340 expect(output.buffer).toMatchSnapshot(); 306 341 }); 307 342
+30 -3
packages/prompts/test/spinner.test.ts
··· 1 1 import { EventEmitter } from 'node:stream'; 2 - import { getColumns } from '@clack/core'; 3 - import color from 'picocolors'; 2 + import { styleText } from 'node:util'; 3 + import { getColumns, updateSettings } from '@clack/core'; 4 4 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest'; 5 5 import * as prompts from '../src/index.js'; 6 6 import { MockWritable } from './test-utils.js'; ··· 26 26 afterEach(() => { 27 27 vi.useRealTimers(); 28 28 vi.restoreAllMocks(); 29 + updateSettings({ withGuide: true }); 29 30 }); 30 31 31 32 test('returns spinner API', () => { ··· 260 261 }); 261 262 262 263 test('custom frame style', () => { 263 - const result = prompts.spinner({ output, styleFrame: color.red }); 264 + const result = prompts.spinner({ output, styleFrame: (text) => styleText('red', text) }); 264 265 265 266 result.start(); 266 267 ··· 422 423 result.start('Testing'); 423 424 424 425 controller.abort(); 426 + 427 + expect(output.buffer).toMatchSnapshot(); 428 + }); 429 + 430 + test('withGuide: false removes guide', () => { 431 + const result = prompts.spinner({ output, withGuide: false }); 432 + 433 + result.start('foo'); 434 + 435 + vi.advanceTimersByTime(80); 436 + 437 + result.stop(); 438 + 439 + expect(output.buffer).toMatchSnapshot(); 440 + }); 441 + 442 + test('global withGuide: false removes guide', () => { 443 + updateSettings({ withGuide: false }); 444 + 445 + const result = prompts.spinner({ output }); 446 + 447 + result.start('foo'); 448 + 449 + vi.advanceTimersByTime(80); 450 + 451 + result.stop(); 425 452 426 453 expect(output.buffer).toMatchSnapshot(); 427 454 });
-6
pnpm-lock.yaml
··· 60 60 61 61 packages/core: 62 62 dependencies: 63 - picocolors: 64 - specifier: ^1.0.0 65 - version: 1.1.1 66 63 sisteransi: 67 64 specifier: ^1.0.5 68 65 version: 1.0.5 ··· 79 76 '@clack/core': 80 77 specifier: workspace:* 81 78 version: link:../core 82 - picocolors: 83 - specifier: ^1.0.0 84 - version: 1.1.1 85 79 sisteransi: 86 80 specifier: ^1.0.5 87 81 version: 1.0.5