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

Configure Feed

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

feat(autocomplete): add wrapping and window limits (#384)

authored by

James Garbutt and committed by
GitHub
(Sep 16, 2025, 4:48 PM +0100) 55645c28 9fd94e2d

+515 -48
+6
.changeset/late-squids-obey.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + "@clack/core": minor 4 + --- 5 + 6 + Support wrapping autocomplete and select prompts.
+1 -1
packages/core/src/index.ts
··· 8 8 export { default as SelectKeyPrompt } from './prompts/select-key.js'; 9 9 export { default as TextPrompt } from './prompts/text.js'; 10 10 export type { ClackState as State } from './types.js'; 11 - export { block, getColumns, isCancel } from './utils/index.js'; 11 + export { block, getColumns, getRows, isCancel } from './utils/index.js'; 12 12 export type { ClackSettings } from './utils/settings.js'; 13 13 export { settings, updateSettings } from './utils/settings.js';
+9 -3
packages/core/src/utils/index.ts
··· 84 84 } 85 85 86 86 export const getColumns = (output: Writable): number => { 87 - const withColumns = output as Writable & { columns?: number }; 88 - if ('columns' in withColumns && typeof withColumns.columns === 'number') { 89 - return withColumns.columns; 87 + if ('columns' in output && typeof output.columns === 'number') { 88 + return output.columns; 90 89 } 91 90 return 80; 92 91 }; 92 + 93 + export const getRows = (output: Writable): number => { 94 + if ('rows' in output && typeof output.rows === 'number') { 95 + return output.rows; 96 + } 97 + return 20; 98 + };
+35 -25
packages/prompts/src/autocomplete.ts
··· 89 89 validate: opts.validate, 90 90 render() { 91 91 // Title and message display 92 - const title = `${color.gray(S_BAR)}\n${symbol(this.state)} ${opts.message}\n`; 92 + const headings = [`${color.gray(S_BAR)}`, `${symbol(this.state)} ${opts.message}`]; 93 93 const userInput = this.userInput; 94 94 const valueAsString = String(this.value ?? ''); 95 95 const options = this.options; ··· 103 103 const selected = getSelectedOptions(this.selectedValues, options); 104 104 const label = 105 105 selected.length > 0 ? ` ${color.dim(selected.map(getLabel).join(', '))}` : ''; 106 - return `${title}${color.gray(S_BAR)}${label}`; 106 + return `${headings.join('\n')}\n${color.gray(S_BAR)}${label}`; 107 107 } 108 108 109 109 case 'cancel': { 110 110 const userInputText = userInput ? ` ${color.strikethrough(color.dim(userInput))}` : ''; 111 - return `${title}${color.gray(S_BAR)}${userInputText}`; 111 + return `${headings.join('\n')}\n${color.gray(S_BAR)}${userInputText}`; 112 112 } 113 113 114 114 default: { ··· 129 129 ) 130 130 : ''; 131 131 132 + // No matches message 133 + const noResults = 134 + this.filteredOptions.length === 0 && userInput 135 + ? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`] 136 + : []; 137 + 138 + const validationError = 139 + this.state === 'error' ? [`${color.yellow(S_BAR)} ${color.yellow(this.error)}`] : []; 140 + 141 + headings.push( 142 + `${color.cyan(S_BAR)}`, 143 + `${color.cyan(S_BAR)} ${color.dim('Search:')}${searchText}${matches}`, 144 + ...noResults, 145 + ...validationError 146 + ); 147 + 148 + // Show instructions 149 + const instructions = [ 150 + `${color.dim('↑/↓')} to select`, 151 + `${color.dim('Enter:')} confirm`, 152 + `${color.dim('Type:')} to search`, 153 + ]; 154 + 155 + const footers = [ 156 + `${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`, 157 + `${color.cyan(S_BAR_END)}`, 158 + ]; 159 + 132 160 // Render options with selection 133 161 const displayOptions = 134 162 this.filteredOptions.length === 0 ··· 136 164 : limitOptions({ 137 165 cursor: this.cursor, 138 166 options: this.filteredOptions, 167 + columnPadding: 3, // for `| ` 168 + rowPadding: headings.length + footers.length, 139 169 style: (option, active) => { 140 170 const label = getLabel(option); 141 171 const hint = ··· 151 181 output: opts.output, 152 182 }); 153 183 154 - // Show instructions 155 - const instructions = [ 156 - `${color.dim('↑/↓')} to select`, 157 - `${color.dim('Enter:')} confirm`, 158 - `${color.dim('Type:')} to search`, 159 - ]; 160 - 161 - // No matches message 162 - const noResults = 163 - this.filteredOptions.length === 0 && userInput 164 - ? [`${color.cyan(S_BAR)} ${color.yellow('No matches found')}`] 165 - : []; 166 - 167 - const validationError = 168 - this.state === 'error' ? [`${color.yellow(S_BAR)} ${color.yellow(this.error)}`] : []; 169 - 170 184 // Return the formatted prompt 171 185 return [ 172 - `${title}${color.cyan(S_BAR)}`, 173 - `${color.cyan(S_BAR)} ${color.dim('Search:')}${searchText}${matches}`, 174 - ...noResults, 175 - ...validationError, 186 + ...headings, 176 187 ...displayOptions.map((option) => `${color.cyan(S_BAR)} ${option}`), 177 - `${color.cyan(S_BAR)} ${color.dim(instructions.join(' • '))}`, 178 - `${color.cyan(S_BAR_END)}`, 188 + ...footers, 179 189 ].join('\n'); 180 190 } 181 191 }
+111 -18
packages/prompts/src/limit-options.ts
··· 1 1 import type { Writable } from 'node:stream'; 2 - import { WriteStream } from 'node:tty'; 2 + import { getColumns, getRows } from '@clack/core'; 3 + import { wrapAnsi } from 'fast-wrap-ansi'; 3 4 import color from 'picocolors'; 4 5 import type { CommonOptions } from './common.js'; 5 6 ··· 8 9 maxItems: number | undefined; 9 10 cursor: number; 10 11 style: (option: TOption, active: boolean) => string; 12 + columnPadding?: number; 13 + rowPadding?: number; 11 14 } 12 15 16 + const trimLines = ( 17 + groups: Array<string[]>, 18 + initialLineCount: number, 19 + startIndex: number, 20 + endIndex: number, 21 + maxLines: number 22 + ) => { 23 + let lineCount = initialLineCount; 24 + let removals = 0; 25 + for (let i = startIndex; i < endIndex; i++) { 26 + const group = groups[i]; 27 + lineCount = lineCount - group.length; 28 + removals++; 29 + if (lineCount <= maxLines) { 30 + break; 31 + } 32 + } 33 + return { lineCount, removals }; 34 + }; 35 + 13 36 export const limitOptions = <TOption>(params: LimitOptionsParams<TOption>): string[] => { 14 37 const { cursor, options, style } = params; 15 38 const output: Writable = params.output ?? process.stdout; 16 - const rows = output instanceof WriteStream && output.rows !== undefined ? output.rows : 10; 39 + const columns = getColumns(output); 40 + const columnPadding = params.columnPadding ?? 0; 41 + const rowPadding = params.rowPadding ?? 4; 42 + const maxWidth = columns - columnPadding; 43 + const rows = getRows(output); 17 44 const overflowFormat = color.dim('...'); 18 45 19 46 const paramMaxItems = params.maxItems ?? Number.POSITIVE_INFINITY; 20 - const outputMaxItems = Math.max(rows - 4, 0); 47 + const outputMaxItems = Math.max(rows - rowPadding, 0); 21 48 // We clamp to minimum 5 because anything less doesn't make sense UX wise 22 - const maxItems = Math.min(outputMaxItems, Math.max(paramMaxItems, 5)); 49 + const maxItems = Math.max(paramMaxItems, 5); 23 50 let slidingWindowLocation = 0; 24 51 25 - if (cursor >= slidingWindowLocation + maxItems - 3) { 52 + if (cursor >= maxItems - 3) { 26 53 slidingWindowLocation = Math.max(Math.min(cursor - maxItems + 3, options.length - maxItems), 0); 27 - } else if (cursor < slidingWindowLocation + 2) { 28 - slidingWindowLocation = Math.max(cursor - 2, 0); 29 54 } 30 55 31 - const shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; 32 - const shouldRenderBottomEllipsis = 56 + let shouldRenderTopEllipsis = maxItems < options.length && slidingWindowLocation > 0; 57 + let shouldRenderBottomEllipsis = 33 58 maxItems < options.length && slidingWindowLocation + maxItems < options.length; 34 59 35 - return options 36 - .slice(slidingWindowLocation, slidingWindowLocation + maxItems) 37 - .map((option, i, arr) => { 38 - const isTopLimit = i === 0 && shouldRenderTopEllipsis; 39 - const isBottomLimit = i === arr.length - 1 && shouldRenderBottomEllipsis; 40 - return isTopLimit || isBottomLimit 41 - ? overflowFormat 42 - : style(option, i + slidingWindowLocation === cursor); 43 - }); 60 + const slidingWindowLocationEnd = Math.min(slidingWindowLocation + maxItems, options.length); 61 + const lineGroups: Array<string[]> = []; 62 + let lineCount = 0; 63 + if (shouldRenderTopEllipsis) { 64 + lineCount++; 65 + } 66 + if (shouldRenderBottomEllipsis) { 67 + lineCount++; 68 + } 69 + 70 + const slidingWindowLocationWithEllipsis = 71 + slidingWindowLocation + (shouldRenderTopEllipsis ? 1 : 0); 72 + const slidingWindowLocationEndWithEllipsis = 73 + slidingWindowLocationEnd - (shouldRenderBottomEllipsis ? 1 : 0); 74 + 75 + for (let i = slidingWindowLocationWithEllipsis; i < slidingWindowLocationEndWithEllipsis; i++) { 76 + const wrappedLines = wrapAnsi(style(options[i], i === cursor), maxWidth).split('\n'); 77 + lineGroups.push(wrappedLines); 78 + lineCount += wrappedLines.length; 79 + } 80 + 81 + if (lineCount > outputMaxItems) { 82 + let precedingRemovals = 0; 83 + let followingRemovals = 0; 84 + let newLineCount = lineCount; 85 + const cursorGroupIndex = cursor - slidingWindowLocationWithEllipsis; 86 + const trimLinesLocal = (startIndex: number, endIndex: number) => 87 + trimLines(lineGroups, newLineCount, startIndex, endIndex, outputMaxItems); 88 + 89 + if (shouldRenderTopEllipsis) { 90 + ({ lineCount: newLineCount, removals: precedingRemovals } = trimLinesLocal( 91 + 0, 92 + cursorGroupIndex 93 + )); 94 + if (newLineCount > outputMaxItems) { 95 + ({ lineCount: newLineCount, removals: followingRemovals } = trimLinesLocal( 96 + cursorGroupIndex + 1, 97 + lineGroups.length 98 + )); 99 + } 100 + } else { 101 + ({ lineCount: newLineCount, removals: followingRemovals } = trimLinesLocal( 102 + cursorGroupIndex + 1, 103 + lineGroups.length 104 + )); 105 + if (newLineCount > outputMaxItems) { 106 + ({ lineCount: newLineCount, removals: precedingRemovals } = trimLinesLocal( 107 + 0, 108 + cursorGroupIndex 109 + )); 110 + } 111 + } 112 + 113 + if (precedingRemovals > 0) { 114 + shouldRenderTopEllipsis = true; 115 + lineGroups.splice(0, precedingRemovals); 116 + } 117 + if (followingRemovals > 0) { 118 + shouldRenderBottomEllipsis = true; 119 + lineGroups.splice(lineGroups.length - followingRemovals, followingRemovals); 120 + } 121 + } 122 + 123 + const result: string[] = []; 124 + if (shouldRenderTopEllipsis) { 125 + result.push(overflowFormat); 126 + } 127 + for (const lineGroup of lineGroups) { 128 + for (const line of lineGroup) { 129 + result.push(line); 130 + } 131 + } 132 + if (shouldRenderBottomEllipsis) { 133 + result.push(overflowFormat); 134 + } 135 + 136 + return result; 44 137 };
+50
packages/prompts/test/__snapshots__/autocomplete.test.ts.snap
··· 46 46 ] 47 47 `; 48 48 49 + exports[`autocomplete > renders bottom ellipsis when items do not fit 1`] = ` 50 + [ 51 + "<cursor.hide>", 52 + "│ 53 + ◆ Select an option 54 + │ 55 + │ Search: _ 56 + │ ● Line 0 57 + │ Line 1 58 + │ Line 2 59 + │ Line 3 60 + │ ... 61 + │ ↑/↓ to select • Enter: confirm • Type: to search 62 + └", 63 + "<cursor.backward count=999><cursor.up count=10>", 64 + "<cursor.down count=1>", 65 + "<erase.down>", 66 + "◇ Select an option 67 + │ Line 0 68 + Line 1 69 + Line 2 70 + Line 3", 71 + " 72 + ", 73 + "<cursor.show>", 74 + ] 75 + `; 76 + 49 77 exports[`autocomplete > renders initial UI with message and instructions 1`] = ` 50 78 [ 51 79 "<cursor.hide>", ··· 90 118 "<erase.down>", 91 119 "◇ Select a fruit 92 120 │ Apple", 121 + " 122 + ", 123 + "<cursor.show>", 124 + ] 125 + `; 126 + 127 + exports[`autocomplete > renders top ellipsis when scrolled down and its do not fit 1`] = ` 128 + [ 129 + "<cursor.hide>", 130 + "│ 131 + ◆ Select an option 132 + │ 133 + │ Search: _ 134 + │ ... 135 + │ ● Option 2 136 + │ ↑/↓ to select • Enter: confirm • Type: to search 137 + └", 138 + "<cursor.backward count=999><cursor.up count=7>", 139 + "<cursor.down count=1>", 140 + "<erase.down>", 141 + "◇ Select an option 142 + │ Option 2", 93 143 " 94 144 ", 95 145 "<cursor.show>",
+57
packages/prompts/test/autocomplete.test.ts
··· 168 168 expect(isCancel(value)).toBe(true); 169 169 expect(output.buffer).toMatchSnapshot(); 170 170 }); 171 + 172 + test('renders bottom ellipsis when items do not fit', async () => { 173 + output.rows = 5; 174 + 175 + const options = [ 176 + { 177 + value: Array.from({ length: 4 }) 178 + .map((_val, index) => `Line ${index}`) 179 + .join('\n'), 180 + }, 181 + { 182 + value: 'Option 2', 183 + }, 184 + ]; 185 + 186 + const result = autocomplete({ 187 + message: 'Select an option', 188 + options, 189 + maxItems: 5, 190 + input, 191 + output, 192 + }); 193 + 194 + input.emit('keypress', '', { name: 'return' }); 195 + await result; 196 + expect(output.buffer).toMatchSnapshot(); 197 + }); 198 + 199 + test('renders top ellipsis when scrolled down and its do not fit', async () => { 200 + output.rows = 5; 201 + 202 + const options = [ 203 + { 204 + value: 'option1', 205 + label: Array.from({ length: 4 }) 206 + .map((_val, index) => `Line ${index}`) 207 + .join('\n'), 208 + }, 209 + { 210 + value: 'option2', 211 + label: 'Option 2', 212 + }, 213 + ]; 214 + 215 + const result = autocomplete({ 216 + message: 'Select an option', 217 + options, 218 + initialValue: 'option2', 219 + maxItems: 5, 220 + input, 221 + output, 222 + }); 223 + 224 + input.emit('keypress', '', { name: 'return' }); 225 + await result; 226 + expect(output.buffer).toMatchSnapshot(); 227 + }); 171 228 }); 172 229 173 230 describe('autocompleteMultiselect', () => {
+244
packages/prompts/test/limit-options.test.ts
··· 1 + import color from 'picocolors'; 2 + import { beforeEach, describe, expect, test } from 'vitest'; 3 + import { type LimitOptionsParams, limitOptions } from '../src/index.js'; 4 + import { MockWritable } from './test-utils.js'; 5 + 6 + describe('limitOptions', () => { 7 + let output: MockWritable; 8 + let options: LimitOptionsParams<{ value: string }>; 9 + 10 + beforeEach(() => { 11 + output = new MockWritable(); 12 + options = { 13 + output, 14 + options: [], 15 + maxItems: undefined, 16 + cursor: 0, 17 + style: (option) => option.value, 18 + columnPadding: undefined, 19 + rowPadding: undefined, 20 + }; 21 + }); 22 + 23 + test('returns all items if they fit', async () => { 24 + options.options = [{ value: 'Item 1' }, { value: 'Item 2' }, { value: 'Item 3' }]; 25 + options.maxItems = 5; 26 + const result = limitOptions(options); 27 + expect(result).toEqual(['Item 1', 'Item 2', 'Item 3']); 28 + }); 29 + 30 + test('clamps to 5 rows minimum', async () => { 31 + options.options = [ 32 + { value: 'Item 1' }, 33 + { value: 'Item 2' }, 34 + { value: 'Item 3' }, 35 + { value: 'Item 4' }, 36 + { value: 'Item 5' }, 37 + { value: 'Item 6' }, 38 + { value: 'Item 7' }, 39 + ]; 40 + options.maxItems = 3; 41 + const result = limitOptions(options); 42 + expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', 'Item 4', color.dim('...')]); 43 + }); 44 + 45 + test('returns sliding window when cursor moves down', async () => { 46 + options.options = [ 47 + { value: 'Item 1' }, 48 + { value: 'Item 2' }, 49 + { value: 'Item 3' }, 50 + { value: 'Item 4' }, 51 + { value: 'Item 5' }, 52 + { value: 'Item 6' }, 53 + { value: 'Item 7' }, 54 + { value: 'Item 8' }, 55 + { value: 'Item 9' }, 56 + { value: 'Item 10' }, 57 + ]; 58 + output.rows = 20; 59 + options.maxItems = 5; 60 + options.cursor = 6; 61 + const result = limitOptions(options); 62 + expect(result).toEqual([color.dim('...'), 'Item 6', 'Item 7', 'Item 8', color.dim('...')]); 63 + }); 64 + 65 + test('returns sliding window near end of list', async () => { 66 + options.options = [ 67 + { value: 'Item 1' }, 68 + { value: 'Item 2' }, 69 + { value: 'Item 3' }, 70 + { value: 'Item 4' }, 71 + { value: 'Item 5' }, 72 + { value: 'Item 6' }, 73 + { value: 'Item 7' }, 74 + { value: 'Item 8' }, 75 + { value: 'Item 9' }, 76 + { value: 'Item 10' }, 77 + ]; 78 + options.maxItems = 5; 79 + options.cursor = 8; 80 + const result = limitOptions(options); 81 + expect(result).toEqual([color.dim('...'), 'Item 7', 'Item 8', 'Item 9', 'Item 10']); 82 + }); 83 + 84 + test('handles empty options list', async () => { 85 + options.options = []; 86 + const result = limitOptions(options); 87 + expect(result).toEqual([]); 88 + }); 89 + 90 + test('if items exceed output height, clamp to fit', async () => { 91 + options.options = [ 92 + { value: 'Item 1' }, 93 + { value: 'Item 2' }, 94 + { value: 'Item 3' }, 95 + { value: 'Item 4' }, 96 + { value: 'Item 5' }, 97 + { value: 'Item 6' }, 98 + { value: 'Item 7' }, 99 + { value: 'Item 8' }, 100 + { value: 'Item 9' }, 101 + { value: 'Item 10' }, 102 + ]; 103 + output.rows = 7; 104 + options.maxItems = 10; 105 + const result = limitOptions(options); 106 + expect(result).toEqual(['Item 1', 'Item 2', 'Item 3', color.dim('...')]); 107 + }); 108 + 109 + test('handle multi-line item clamping (start)', async () => { 110 + options.options = [ 111 + { value: 'Item 1' }, 112 + { value: 'Item 2' }, 113 + { 114 + value: Array.from({ length: 4 }) 115 + .map((_val, index) => `A long item that will take up a lot of space (line ${index})`) 116 + .join('\n'), 117 + }, 118 + { value: 'Item 4' }, 119 + { value: 'Item 5' }, 120 + { value: 'Item 6' }, 121 + { value: 'Item 7' }, 122 + { value: 'Item 8' }, 123 + { value: 'Item 9' }, 124 + { value: 'Item 10' }, 125 + ]; 126 + output.rows = 14; 127 + options.maxItems = 10; 128 + const result = limitOptions(options); 129 + expect(result).toEqual([ 130 + 'Item 1', 131 + 'Item 2', 132 + 'A long item that will take up a lot of space (line 0)', 133 + 'A long item that will take up a lot of space (line 1)', 134 + 'A long item that will take up a lot of space (line 2)', 135 + 'A long item that will take up a lot of space (line 3)', 136 + 'Item 4', 137 + 'Item 5', 138 + 'Item 6', 139 + 'Item 7', 140 + 'Item 8', 141 + color.dim('...'), 142 + ]); 143 + }); 144 + 145 + test('handle multi-line item clamping (middle)', async () => { 146 + options.options = [ 147 + { value: 'Item 1' }, 148 + { value: 'Item 2' }, 149 + { value: 'Item 3' }, 150 + { value: 'Item 4' }, 151 + { 152 + value: Array.from({ length: 4 }) 153 + .map((_val, index) => `A long item that will take up a lot of space (line ${index})`) 154 + .join('\n'), 155 + }, 156 + { value: 'Item 6' }, 157 + { value: 'Item 7' }, 158 + { value: 'Item 8' }, 159 + { value: 'Item 9' }, 160 + { value: 'Item 10' }, 161 + ]; 162 + output.rows = 14; 163 + options.maxItems = 10; 164 + options.cursor = 7; 165 + const result = limitOptions(options); 166 + expect(result).toEqual([ 167 + color.dim('...'), 168 + 'Item 2', 169 + 'Item 3', 170 + 'Item 4', 171 + 'A long item that will take up a lot of space (line 0)', 172 + 'A long item that will take up a lot of space (line 1)', 173 + 'A long item that will take up a lot of space (line 2)', 174 + 'A long item that will take up a lot of space (line 3)', 175 + 'Item 6', 176 + 'Item 7', 177 + 'Item 8', 178 + color.dim('...'), 179 + ]); 180 + }); 181 + 182 + test('handle multi-line item clamping (end)', async () => { 183 + options.options = [ 184 + { value: 'Item 1' }, 185 + { value: 'Item 2' }, 186 + { value: 'Item 3' }, 187 + { value: 'Item 4' }, 188 + { value: 'Item 5' }, 189 + { value: 'Item 6' }, 190 + { value: 'Item 7' }, 191 + { 192 + value: Array.from({ length: 4 }) 193 + .map((_val, index) => `A long item that will take up a lot of space (line ${index})`) 194 + .join('\n'), 195 + }, 196 + { value: 'Item 9' }, 197 + { value: 'Item 10' }, 198 + ]; 199 + output.rows = 14; 200 + options.maxItems = 10; 201 + options.cursor = 9; 202 + const result = limitOptions(options); 203 + expect(result).toEqual([ 204 + color.dim('...'), 205 + 'Item 4', 206 + 'Item 5', 207 + 'Item 6', 208 + 'Item 7', 209 + 'A long item that will take up a lot of space (line 0)', 210 + 'A long item that will take up a lot of space (line 1)', 211 + 'A long item that will take up a lot of space (line 2)', 212 + 'A long item that will take up a lot of space (line 3)', 213 + 'Item 9', 214 + 'Item 10', 215 + ]); 216 + }); 217 + 218 + test('style option is used to style lines', async () => { 219 + options.options = [{ value: 'Item 1' }, { value: 'Item 2' }, { value: 'Item 3' }]; 220 + options.maxItems = 5; 221 + options.style = (option) => `-- ${option.value} --`; 222 + const result = limitOptions(options); 223 + expect(result).toEqual(['-- Item 1 --', '-- Item 2 --', '-- Item 3 --']); 224 + }); 225 + 226 + test('style option styles across multi-line items', async () => { 227 + options.options = [{ value: 'Item 1' }, { value: 'Item 2' }, { value: 'Item 3\nContinued' }]; 228 + options.maxItems = 5; 229 + options.style = (option) => `-- ${option.value} --`; 230 + const result = limitOptions(options); 231 + expect(result).toEqual(['-- Item 1 --', '-- Item 2 --', '-- Item 3', 'Continued --']); 232 + }); 233 + 234 + test('style option receives correct cursor index', async () => { 235 + options.options = [{ value: 'Item 1' }, { value: 'Item 2' }, { value: 'Item 3' }]; 236 + options.maxItems = 5; 237 + options.cursor = 1; 238 + options.style = (option, isSelected) => { 239 + return isSelected ? `-- ${option.value} --` : option.value; 240 + }; 241 + const result = limitOptions(options); 242 + expect(result).toEqual(['Item 1', '-- Item 2 --', 'Item 3']); 243 + }); 244 + });
+1
packages/prompts/test/test-utils.ts
··· 4 4 public buffer: string[] = []; 5 5 public isTTY = false; 6 6 public columns = 80; 7 + public rows = 20; 7 8 8 9 _write( 9 10 chunk: any,
+1 -1
tsconfig.json
··· 17 17 "@clack/prompts": ["./packages/prompts/src/index.ts"] 18 18 } 19 19 }, 20 - "include": ["packages/*/src/**/*"] 20 + "include": ["packages/*/src/**/*.ts", "packages/*/test/**/*.ts"] 21 21 }