[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' of https://github.com/Mist3rBru/clack into fix/spinner-exit

Bruno Rocha (Aug 9, 2023, 12:05 PM -0300) 03ee8e54 5f1af37d

+97 -12
+5
.changeset/eight-bikes-repair.md
··· 1 + --- 2 + '@clack/prompts': minor 3 + --- 4 + 5 + add maxItems option to select prompt
+5
.changeset/loud-bugs-move.md
··· 1 + --- 2 + '@clack/prompts': minor 3 + --- 4 + 5 + added a new method called `spinner.message(msg: string)`
+5
.changeset/odd-avocados-smile.md
··· 1 + --- 2 + '@clack/prompts': patch 3 + --- 4 + 5 + Fixes cases where the note title length was miscalculated due to ansi characters
+5
.changeset/twelve-shrimps-report.md
··· 1 + --- 2 + '@clack/core': patch 3 + --- 4 + 5 + fix: restore raw mode on unblock
+2 -1
examples/basic/package.json
··· 9 9 "picocolors": "^1.0.0" 10 10 }, 11 11 "scripts": { 12 - "start": "jiti ./index.ts" 12 + "start": "jiti ./index.ts", 13 + "spinner": "jiti ./spinner.ts" 13 14 }, 14 15 "devDependencies": { 15 16 "jiti": "^1.17.0"
+22
examples/basic/spinner.ts
··· 1 + import * as p from '@clack/prompts'; 2 + 3 + p.intro('spinner start...'); 4 + 5 + const spin = p.spinner(); 6 + const total = 10000; 7 + let progress = 0; 8 + spin.start(); 9 + 10 + new Promise((resolve) => { 11 + const timer = setInterval(() => { 12 + progress = Math.min(total, progress + 100); 13 + if (progress >= total) { 14 + clearInterval(timer); 15 + resolve(true); 16 + } 17 + spin.message(`Loading packages [${progress}/${total}]`); // <=== 18 + }, 100); 19 + }).then(() => { 20 + spin.stop(`Done`); 21 + p.outro('spinner stop...'); 22 + });
+1
packages/core/src/utils.ts
··· 40 40 return () => { 41 41 input.off('keypress', clear); 42 42 if (hideCursor) process.stdout.write(cursor.show); 43 + if (input.isTTY) input.setRawMode(false); 43 44 44 45 // @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907 45 46 rl.terminal = false;
+52 -11
packages/prompts/src/index.ts
··· 178 178 message: string; 179 179 options: Options; 180 180 initialValue?: Value; 181 + maxItems?: number; 181 182 } 182 183 183 184 export const select = <Options extends Option<Value>[], Value>( ··· 196 197 } 197 198 return `${color.dim(S_RADIO_INACTIVE)} ${color.dim(label)}`; 198 199 }; 200 + 201 + let slidingWindowLocation = 0; 199 202 200 203 return new SelectPrompt({ 201 204 options: opts.options, ··· 212 215 'cancelled' 213 216 )}\n${color.gray(S_BAR)}`; 214 217 default: { 218 + // We clamp to minimum 5 because anything less doesn't make sense UX wise 219 + const maxItems = opts.maxItems === undefined ? Infinity : Math.max(opts.maxItems, 5); 220 + if (this.cursor >= slidingWindowLocation + maxItems - 3) { 221 + slidingWindowLocation = Math.max( 222 + Math.min(this.cursor - maxItems + 3, this.options.length - maxItems), 223 + 0 224 + ); 225 + } else if (this.cursor < slidingWindowLocation + 2) { 226 + slidingWindowLocation = Math.max(this.cursor - 2, 0); 227 + } 228 + 229 + const shouldRenderTopEllipsis = 230 + maxItems < this.options.length && slidingWindowLocation > 0; 231 + const shouldRenderBottomEllipsis = 232 + maxItems < this.options.length && 233 + slidingWindowLocation + maxItems < this.options.length; 234 + 215 235 return `${title}${color.cyan(S_BAR)} ${this.options 216 - .map((option, i) => opt(option, i === this.cursor ? 'active' : 'inactive')) 236 + .slice(slidingWindowLocation, slidingWindowLocation + maxItems) 237 + .map((option, i, arr) => { 238 + if (i === 0 && shouldRenderTopEllipsis) { 239 + return color.dim('...'); 240 + } else if (i === arr.length - 1 && shouldRenderBottomEllipsis) { 241 + return color.dim('...'); 242 + } else { 243 + return opt( 244 + option, 245 + i + slidingWindowLocation === this.cursor ? 'active' : 'inactive' 246 + ); 247 + } 248 + }) 217 249 .join(`\n${color.cyan(S_BAR)} `)}\n${color.cyan(S_BAR_END)}\n`; 218 250 } 219 251 } ··· 534 566 const strip = (str: string) => str.replace(ansiRegex(), ''); 535 567 export const note = (message = '', title = '') => { 536 568 const lines = `\n${message}\n`.split('\n'); 569 + const titleLen = strip(title).length; 537 570 const len = 538 571 Math.max( 539 572 lines.reduce((sum, ln) => { 540 573 ln = strip(ln); 541 574 return ln.length > sum ? ln.length : sum; 542 575 }, 0), 543 - strip(title).length 576 + titleLen 544 577 ) + 2; 545 578 const msg = lines 546 579 .map( ··· 552 585 .join('\n'); 553 586 process.stdout.write( 554 587 `${color.gray(S_BAR)}\n${color.green(S_STEP_SUBMIT)} ${color.reset(title)} ${color.gray( 555 - S_BAR_H.repeat(Math.max(len - title.length - 1, 1)) + S_CORNER_TOP_RIGHT 588 + S_BAR_H.repeat(Math.max(len - titleLen - 1, 1)) + S_CORNER_TOP_RIGHT 556 589 )}\n${msg}\n${color.gray(S_CONNECT_LEFT + S_BAR_H.repeat(len + 2) + S_CORNER_BOTTOM_RIGHT)}\n` 557 590 ); 558 591 }; ··· 603 636 }; 604 637 605 638 export const spinner = () => { 639 + const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; 640 + const delay = unicode ? 80 : 120; 641 + 606 642 let unblock: () => void; 607 643 let loop: NodeJS.Timer; 608 644 let isSpinnerActive: boolean = false; 609 - const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0']; 610 - const delay = unicode ? 80 : 120; 645 + let _message: string = ''; 611 646 612 - const start = (message: string = ''): void => { 647 + const start = (msg: string = ''): void => { 613 648 isSpinnerActive = true; 614 649 unblock = block(); 615 - message = message.replace(/\.+$/, ''); 650 + _message = msg.replace(/\.+$/, ''); 616 651 process.stdout.write(`${color.gray(S_BAR)}\n`); 617 652 let frameIndex = 0; 618 653 let dotsTimer = 0; ··· 621 656 const loadingDots = '.'.repeat(Math.floor(dotsTimer)).slice(0, 3); 622 657 process.stdout.write(cursor.move(-999, 0)); 623 658 process.stdout.write(erase.down(1)); 624 - process.stdout.write(`${frame} ${message}${loadingDots}`); 659 + process.stdout.write(`${frame} ${_message}${loadingDots}`); 625 660 frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0; 626 661 dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0; 627 662 }, delay); 628 663 }; 629 664 630 - const stop = (message: string = '', code: number = 0): void => { 665 + const stop = (msg: string = '', code: number = 0): void => { 666 + _message = msg ?? _message 631 667 isSpinnerActive = false; 632 668 clearInterval(loop); 633 669 const step = ··· 642 678 unblock(); 643 679 }; 644 680 681 + const message = (msg: string = ''): void => { 682 + _message = msg ?? _message; 683 + }; 684 + 645 685 const handleExit = (code: number) => { 646 - const message = code > 1 ? 'Something went wrong' : 'Canceled'; 647 - if (isSpinnerActive) stop(message, code); 686 + const msg = code > 1 ? 'Something went wrong' : 'Canceled'; 687 + if (isSpinnerActive) stop(msg, code); 648 688 }; 649 689 650 690 // Reference: https://nodejs.org/api/process.html#event-uncaughtexception ··· 659 699 return { 660 700 start, 661 701 stop, 702 + message, 662 703 }; 663 704 }; 664 705