[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: add `groupSpacing` option to group multi select (#256)

Co-authored-by: Nate Moore <natemoo-re@users.noreply.github.com>

authored by

James Garbutt
Nate Moore
and committed by
GitHub
(Apr 10, 2025, 1:46 PM +0100) 44df9af1 59682728

+235 -7
+5
.changeset/happy-parents-explain.md
··· 1 + --- 2 + "@clack/prompts": minor 3 + --- 4 + 5 + Adds a new `groupSpacing` option to grouped multi-select prompts. If set to an integer greater than 0, it will add that number of new lines between each group.
+176
packages/prompts/src/__snapshots__/index.test.ts.snap
··· 380 380 ] 381 381 `; 382 382 383 + exports[`prompts (isCI = false) > groupMultiselect > groupSpacing > negative spacing is ignored 1`] = ` 384 + [ 385 + "[?25l", 386 + "│ 387 + ◆ foo 388 + │ ◻ group1 389 + │ └ ◻ group1value0 390 + │ ◻ group2 391 + │ └ ◻ group2value0 392 + └ 393 + ", 394 + "", 395 + "", 396 + "", 397 + "│ ◻ group1 398 + │ └ ◻ group1value0 399 + │ ◻ group2 400 + │ └ ◻ group2value0 401 + └ 402 + ", 403 + "", 404 + "", 405 + "", 406 + "│ ◼ group1 407 + │ └ ◼ group1value0 408 + │ ◻ group2 409 + │ └ ◻ group2value0 410 + └ 411 + ", 412 + "", 413 + "", 414 + "", 415 + "◇ foo 416 + │ group1value0", 417 + " 418 + ", 419 + "[?25h", 420 + ] 421 + `; 422 + 423 + exports[`prompts (isCI = false) > groupMultiselect > groupSpacing > renders spaced groups 1`] = ` 424 + [ 425 + "[?25l", 426 + "│ 427 + ◆ foo 428 + │ 429 + │ 430 + │ ◻ group1 431 + │ └ ◻ group1value0 432 + │ 433 + │ 434 + │ ◻ group2 435 + │ └ ◻ group2value0 436 + └ 437 + ", 438 + "", 439 + "", 440 + "", 441 + "│ ◻ group1 442 + │ └ ◻ group1value0 443 + │ 444 + │ 445 + │ ◻ group2 446 + │ └ ◻ group2value0 447 + └ 448 + ", 449 + "", 450 + "", 451 + "", 452 + "│ ◼ group1 453 + │ └ ◼ group1value0 454 + │ 455 + │ 456 + │ ◻ group2 457 + │ └ ◻ group2value0 458 + └ 459 + ", 460 + "", 461 + "", 462 + "", 463 + "◇ foo 464 + │ group1value0", 465 + " 466 + ", 467 + "[?25h", 468 + ] 469 + `; 470 + 383 471 exports[`prompts (isCI = false) > groupMultiselect > initial values can be set 1`] = ` 384 472 [ 385 473 "[?25l", ··· 2148 2236 "", 2149 2237 "◇ foo 2150 2238 │ group1value1", 2239 + " 2240 + ", 2241 + "[?25h", 2242 + ] 2243 + `; 2244 + 2245 + exports[`prompts (isCI = true) > groupMultiselect > groupSpacing > negative spacing is ignored 1`] = ` 2246 + [ 2247 + "[?25l", 2248 + "│ 2249 + ◆ foo 2250 + │ ◻ group1 2251 + │ └ ◻ group1value0 2252 + │ ◻ group2 2253 + │ └ ◻ group2value0 2254 + └ 2255 + ", 2256 + "", 2257 + "", 2258 + "", 2259 + "│ ◻ group1 2260 + │ └ ◻ group1value0 2261 + │ ◻ group2 2262 + │ └ ◻ group2value0 2263 + └ 2264 + ", 2265 + "", 2266 + "", 2267 + "", 2268 + "│ ◼ group1 2269 + │ └ ◼ group1value0 2270 + │ ◻ group2 2271 + │ └ ◻ group2value0 2272 + └ 2273 + ", 2274 + "", 2275 + "", 2276 + "", 2277 + "◇ foo 2278 + │ group1value0", 2279 + " 2280 + ", 2281 + "[?25h", 2282 + ] 2283 + `; 2284 + 2285 + exports[`prompts (isCI = true) > groupMultiselect > groupSpacing > renders spaced groups 1`] = ` 2286 + [ 2287 + "[?25l", 2288 + "│ 2289 + ◆ foo 2290 + │ 2291 + │ 2292 + │ ◻ group1 2293 + │ └ ◻ group1value0 2294 + │ 2295 + │ 2296 + │ ◻ group2 2297 + │ └ ◻ group2value0 2298 + └ 2299 + ", 2300 + "", 2301 + "", 2302 + "", 2303 + "│ ◻ group1 2304 + │ └ ◻ group1value0 2305 + │ 2306 + │ 2307 + │ ◻ group2 2308 + │ └ ◻ group2value0 2309 + └ 2310 + ", 2311 + "", 2312 + "", 2313 + "", 2314 + "│ ◼ group1 2315 + │ └ ◼ group1value0 2316 + │ 2317 + │ 2318 + │ ◻ group2 2319 + │ └ ◻ group2value0 2320 + └ 2321 + ", 2322 + "", 2323 + "", 2324 + "", 2325 + "◇ foo 2326 + │ group1value0", 2151 2327 " 2152 2328 ", 2153 2329 "[?25h",
+44
packages/prompts/src/index.test.ts
··· 1204 1204 expect(value).toEqual([value0]); 1205 1205 expect(output.buffer).toMatchSnapshot(); 1206 1206 }); 1207 + 1208 + describe('groupSpacing', () => { 1209 + test('renders spaced groups', async () => { 1210 + const result = prompts.groupMultiselect({ 1211 + message: 'foo', 1212 + input, 1213 + output, 1214 + options: { 1215 + group1: [{ value: 'group1value0' }], 1216 + group2: [{ value: 'group2value0' }], 1217 + }, 1218 + groupSpacing: 2, 1219 + }); 1220 + 1221 + input.emit('keypress', '', { name: 'down' }); 1222 + input.emit('keypress', '', { name: 'space' }); 1223 + input.emit('keypress', '', { name: 'return' }); 1224 + 1225 + await result; 1226 + 1227 + expect(output.buffer).toMatchSnapshot(); 1228 + }); 1229 + 1230 + test('negative spacing is ignored', async () => { 1231 + const result = prompts.groupMultiselect({ 1232 + message: 'foo', 1233 + input, 1234 + output, 1235 + options: { 1236 + group1: [{ value: 'group1value0' }], 1237 + group2: [{ value: 'group2value0' }], 1238 + }, 1239 + groupSpacing: -2, 1240 + }); 1241 + 1242 + input.emit('keypress', '', { name: 'down' }); 1243 + input.emit('keypress', '', { name: 'space' }); 1244 + input.emit('keypress', '', { name: 'return' }); 1245 + 1246 + await result; 1247 + 1248 + expect(output.buffer).toMatchSnapshot(); 1249 + }); 1250 + }); 1207 1251 }); 1208 1252 });
+10 -7
packages/prompts/src/index.ts
··· 489 489 required?: boolean; 490 490 cursorAt?: Value; 491 491 selectableGroups?: boolean; 492 + groupSpacing?: number; 492 493 } 493 494 export const groupMultiselect = <Value>(opts: GroupMultiSelectOptions<Value>) => { 494 - const { selectableGroups = true } = opts; 495 + const { selectableGroups = true, groupSpacing = 0 } = opts; 495 496 const opt = ( 496 497 option: Option<Value>, 497 498 state: ··· 510 511 const next = isItem && (options[options.indexOf(option) + 1] ?? { group: true }); 511 512 const isLast = isItem && (next as any).group === true; 512 513 const prefix = isItem ? (selectableGroups ? `${isLast ? S_BAR_END : S_BAR} ` : ' ') : ''; 514 + const spacingPrefix = 515 + groupSpacing > 0 && !isItem ? `\n${color.cyan(S_BAR)} `.repeat(groupSpacing) : ''; 513 516 514 517 if (state === 'active') { 515 - return `${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ 518 + return `${spacingPrefix}${color.dim(prefix)}${color.cyan(S_CHECKBOX_ACTIVE)} ${label} ${ 516 519 option.hint ? color.dim(`(${option.hint})`) : '' 517 520 }`; 518 521 } 519 522 if (state === 'group-active') { 520 - return `${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; 523 + return `${spacingPrefix}${prefix}${color.cyan(S_CHECKBOX_ACTIVE)} ${color.dim(label)}`; 521 524 } 522 525 if (state === 'group-active-selected') { 523 - return `${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; 526 + return `${spacingPrefix}${prefix}${color.green(S_CHECKBOX_SELECTED)} ${color.dim(label)}`; 524 527 } 525 528 if (state === 'selected') { 526 529 const selectedCheckbox = isItem || selectableGroups ? color.green(S_CHECKBOX_SELECTED) : ''; 527 - return `${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${ 530 + return `${spacingPrefix}${color.dim(prefix)}${selectedCheckbox} ${color.dim(label)} ${ 528 531 option.hint ? color.dim(`(${option.hint})`) : '' 529 532 }`; 530 533 } ··· 532 535 return `${color.strikethrough(color.dim(label))}`; 533 536 } 534 537 if (state === 'active-selected') { 535 - return `${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ 538 + return `${spacingPrefix}${color.dim(prefix)}${color.green(S_CHECKBOX_SELECTED)} ${label} ${ 536 539 option.hint ? color.dim(`(${option.hint})`) : '' 537 540 }`; 538 541 } ··· 540 543 return `${color.dim(label)}`; 541 544 } 542 545 const unselectedCheckbox = isItem || selectableGroups ? color.dim(S_CHECKBOX_INACTIVE) : ''; 543 - return `${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; 546 + return `${spacingPrefix}${color.dim(prefix)}${unselectedCheckbox} ${color.dim(label)}`; 544 547 }; 545 548 546 549 return new GroupMultiSelectPrompt({