Skip to content

Commit 0cdd5ab

Browse files
committed
Merge branch 'main' of https://github.com/Mist3rBru/clack into core-multiline
2 parents de45a4e + 5529c89 commit 0cdd5ab

File tree

10 files changed

+195
-19
lines changed

10 files changed

+195
-19
lines changed

examples/basic/package.json

+3-1
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@
1010
},
1111
"scripts": {
1212
"start": "jiti ./index.ts",
13+
"stream": "jiti ./stream.ts",
1314
"spinner": "jiti ./spinner.ts",
14-
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts"
15+
"spinner-ci": "npx cross-env CI=\"true\" jiti ./spinner-ci.ts",
16+
"spinner-timer": "jiti ./spinner-timer.ts"
1517
},
1618
"devDependencies": {
1719
"jiti": "^1.17.0"

examples/basic/spinner-ci.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* - There will be no loading dots animation, instead it will be always `...`
88
* - Instead of erase the previous message, action that is blocked during CI, it will just write a new one.
99
*
10-
* Issue: https://github.com/natemoo-re/clack/issues/168
10+
* Issue: https://github.com/bombshell-dev/clack/issues/168
1111
*/
1212
import * as p from '@clack/prompts';
1313

examples/basic/spinner-timer.ts

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import * as p from '@clack/prompts';
2+
3+
p.intro('spinner start...');
4+
5+
async function main() {
6+
const spin = p.spinner({ indicator: 'timer' });
7+
8+
spin.start('First spinner');
9+
10+
await sleep(3_000);
11+
12+
spin.stop('Done first spinner');
13+
14+
spin.start('Second spinner');
15+
await sleep(5_000);
16+
17+
spin.stop('Done second spinner');
18+
19+
p.outro('spinner stop.');
20+
}
21+
22+
function sleep(ms: number) {
23+
return new Promise((resolve) => setTimeout(resolve, ms));
24+
}
25+
26+
main();

examples/basic/stream.ts

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { setTimeout } from 'node:timers/promises';
2+
import * as p from '@clack/prompts';
3+
import color from 'picocolors';
4+
5+
async function main() {
6+
console.clear();
7+
8+
await setTimeout(1000);
9+
10+
p.intro(`${color.bgCyan(color.black(' create-app '))}`);
11+
12+
await p.stream.step(
13+
(async function* () {
14+
for (const line of lorem) {
15+
for (const word of line.split(' ')) {
16+
yield word;
17+
yield ' ';
18+
await setTimeout(200);
19+
}
20+
yield '\n';
21+
if (line !== lorem.at(-1)) {
22+
await setTimeout(1000);
23+
}
24+
}
25+
})()
26+
);
27+
28+
p.outro(`Problems? ${color.underline(color.cyan('https://example.com/issues'))}`);
29+
}
30+
31+
const lorem = [
32+
'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.',
33+
'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.',
34+
];
35+
36+
main().catch(console.error);

packages/core/package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,13 @@
1515
"types": "./dist/index.d.ts",
1616
"repository": {
1717
"type": "git",
18-
"url": "https://github.com/natemoo-re/clack",
18+
"url": "git+https://github.com/bombshell-dev/clack.git",
1919
"directory": "packages/core"
2020
},
2121
"bugs": {
22-
"url": "https://github.com/natemoo-re/clack/issues"
22+
"url": "https://github.com/bombshell-dev/clack/issues"
2323
},
24-
"homepage": "https://github.com/natemoo-re/clack/tree/main/packages/core#readme",
24+
"homepage": "https://github.com/bombshell-dev/clack/tree/main/packages/core#readme",
2525
"files": ["dist", "CHANGELOG.md"],
2626
"keywords": [
2727
"ask",

packages/core/src/utils/index.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export function block({
6161
input.off('keypress', clear);
6262
if (hideCursor) output.write(cursor.show);
6363

64-
// Prevent Windows specific issues: https://github.com/natemoo-re/clack/issues/176
64+
// Prevent Windows specific issues: https://github.com/bombshell-dev/clack/issues/176
6565
if (input.isTTY && !isWindows) input.setRawMode(false);
6666

6767
// @ts-expect-error fix for https://github.com/nodejs/node/issues/31762#issuecomment-1441223907

packages/prompts/CHANGELOG.md

+27
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,32 @@
11
# @clack/prompts
22

3+
## 0.10.0
4+
5+
### Minor Changes
6+
7+
- 613179d: Adds a new `indicator` option to `spinner`, which supports the original `"dots"` loading animation or a new `"timer"` loading animation.
8+
9+
```ts
10+
import * as p from "@clack/prompts";
11+
12+
const spin = p.spinner({ indicator: "timer" });
13+
spin.start("Loading");
14+
await sleep(3000);
15+
spin.stop("Loaded");
16+
```
17+
18+
- a38b2bc: Adds `stream` API which provides the same methods as `log`, but for iterable (even async) message streams. This is particularly useful for AI responses which are dynamically generated by LLMs.
19+
20+
```ts
21+
import * as p from "@clack/prompts";
22+
23+
await p.stream.step(
24+
(async function* () {
25+
yield* generateLLMResponse(question);
26+
})()
27+
);
28+
```
29+
330
## 0.9.1
431

532
### Patch Changes

packages/prompts/README.md

+18-2
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Effortlessly build beautiful command-line apps 🪄 [Try the demo](https://stackblitz.com/edit/clack-prompts?file=index.js)
44

5-
![clack-prompt](https://github.com/natemoo-re/clack/blob/main/.github/assets/clack-demo.gif)
5+
![clack-prompt](https://github.com/bombshell-dev/clack/blob/main/.github/assets/clack-demo.gif)
66

77
---
88

@@ -188,4 +188,20 @@ log.error('Error!');
188188
log.message('Hello, World', { symbol: color.cyan('~') });
189189
```
190190

191-
[clack-log-prompts](https://github.com/natemoo-re/clack/blob/main/.github/assets/clack-logs.png)
191+
192+
### Stream
193+
194+
When interacting with dynamic LLMs or other streaming message providers, use the `stream` APIs to log messages from an iterable, even an async one.
195+
196+
```js
197+
import { stream } from '@clack/prompts';
198+
199+
stream.info((function *() { yield 'Info!'; })());
200+
stream.success((function *() { yield 'Success!'; })());
201+
stream.step((function *() { yield 'Step!'; })());
202+
stream.warn((function *() { yield 'Warn!'; })());
203+
stream.error((function *() { yield 'Error!'; })());
204+
stream.message((function *() { yield 'Hello'; yield ", World" })(), { symbol: color.cyan('~') });
205+
```
206+
207+
[clack-log-prompts](https://github.com/bombshell-dev/clack/blob/main/.github/assets/clack-logs.png)

packages/prompts/package.json

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@clack/prompts",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"type": "module",
55
"main": "./dist/index.cjs",
66
"module": "./dist/index.mjs",
@@ -15,13 +15,13 @@
1515
"types": "./dist/index.d.ts",
1616
"repository": {
1717
"type": "git",
18-
"url": "https://github.com/natemoo-re/clack",
18+
"url": "git+https://github.com/bombshell-dev/clack.git",
1919
"directory": "packages/prompts"
2020
},
2121
"bugs": {
22-
"url": "https://github.com/natemoo-re/clack/issues"
22+
"url": "https://github.com/bombshell-dev/clack/issues"
2323
},
24-
"homepage": "https://github.com/natemoo-re/clack/tree/main/packages/prompts#readme",
24+
"homepage": "https://github.com/bombshell-dev/clack/tree/main/packages/prompts#readme",
2525
"files": ["dist", "CHANGELOG.md"],
2626
"author": {
2727
"name": "Nate Moore",

packages/prompts/src/index.ts

+76-7
Original file line numberDiff line numberDiff line change
@@ -838,7 +838,56 @@ export const log = {
838838
},
839839
};
840840

841-
export const spinner = () => {
841+
const prefix = `${color.gray(S_BAR)} `;
842+
export const stream = {
843+
message: async (
844+
iterable: Iterable<string> | AsyncIterable<string>,
845+
{ symbol = color.gray(S_BAR) }: LogMessageOptions = {}
846+
) => {
847+
process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `);
848+
let lineWidth = 3;
849+
for await (let chunk of iterable) {
850+
chunk = chunk.replace(/\n/g, `\n${prefix}`);
851+
if (chunk.includes('\n')) {
852+
lineWidth = 3 + strip(chunk.slice(chunk.lastIndexOf('\n'))).length;
853+
}
854+
const chunkLen = strip(chunk).length;
855+
if (lineWidth + chunkLen < process.stdout.columns) {
856+
lineWidth += chunkLen;
857+
process.stdout.write(chunk);
858+
} else {
859+
process.stdout.write(`\n${prefix}${chunk.trimStart()}`);
860+
lineWidth = 3 + strip(chunk.trimStart()).length;
861+
}
862+
}
863+
process.stdout.write('\n');
864+
},
865+
info: (iterable: Iterable<string> | AsyncIterable<string>) => {
866+
return stream.message(iterable, { symbol: color.blue(S_INFO) });
867+
},
868+
success: (iterable: Iterable<string> | AsyncIterable<string>) => {
869+
return stream.message(iterable, { symbol: color.green(S_SUCCESS) });
870+
},
871+
step: (iterable: Iterable<string> | AsyncIterable<string>) => {
872+
return stream.message(iterable, { symbol: color.green(S_STEP_SUBMIT) });
873+
},
874+
warn: (iterable: Iterable<string> | AsyncIterable<string>) => {
875+
return stream.message(iterable, { symbol: color.yellow(S_WARN) });
876+
},
877+
/** alias for `log.warn()`. */
878+
warning: (iterable: Iterable<string> | AsyncIterable<string>) => {
879+
return stream.warn(iterable);
880+
},
881+
error: (iterable: Iterable<string> | AsyncIterable<string>) => {
882+
return stream.message(iterable, { symbol: color.red(S_ERROR) });
883+
},
884+
};
885+
886+
export interface SpinnerOptions {
887+
indicator?: 'dots' | 'timer';
888+
}
889+
890+
export const spinner = ({ indicator = 'dots' }: SpinnerOptions = {}) => {
842891
const frames = unicode ? ['◒', '◐', '◓', '◑'] : ['•', 'o', 'O', '0'];
843892
const delay = unicode ? 80 : 120;
844893
const isCI = process.env.CI === 'true';
@@ -848,6 +897,7 @@ export const spinner = () => {
848897
let isSpinnerActive = false;
849898
let _message = '';
850899
let _prevMessage: string | undefined = undefined;
900+
let _origin: number = performance.now();
851901

852902
const formatMessage = (symbol: string, msg: string): string => {
853903
return format(msg, {
@@ -899,13 +949,21 @@ export const spinner = () => {
899949
return msg.replace(/\.+$/, '');
900950
};
901951

952+
const formatTimer = (origin: number): string => {
953+
const duration = (performance.now() - origin) / 1000;
954+
const min = Math.floor(duration / 60);
955+
const secs = Math.floor(duration % 60);
956+
return min > 0 ? `[${min}m ${secs}s]` : `[${secs}s]`;
957+
};
958+
902959
const start = (msg = ''): void => {
903960
isSpinnerActive = true;
904961
unblock = block();
905962
_message = parseMessage(msg);
963+
_origin = performance.now();
906964
process.stdout.write(`${color.gray(S_BAR)}\n`);
907965
let frameIndex = 0;
908-
let dotsTimer = 0;
966+
let indicatorTimer = 0;
909967
registerHooks();
910968
loop = setInterval(() => {
911969
if (isCI && _message === _prevMessage) {
@@ -914,11 +972,18 @@ export const spinner = () => {
914972
clearPrevMessage();
915973
_prevMessage = _message;
916974
const frame = color.magenta(frames[frameIndex]);
917-
const loadingDots = isCI ? '...' : '.'.repeat(Math.floor(dotsTimer)).slice(0, 3);
918-
_prevMessage = _message;
919-
process.stdout.write(formatMessage(frame, _message + loadingDots));
975+
976+
if (isCI) {
977+
process.stdout.write(`${frame} ${_message}...`);
978+
} else if (indicator === 'timer') {
979+
process.stdout.write(`${frame} ${_message} ${formatTimer(_origin)}`);
980+
} else {
981+
const loadingDots = '.'.repeat(Math.floor(indicatorTimer)).slice(0, 3);
982+
process.stdout.write(`${frame} ${_message}${loadingDots}`);
983+
}
984+
920985
frameIndex = frameIndex + 1 < frames.length ? frameIndex + 1 : 0;
921-
dotsTimer = dotsTimer < frames.length ? dotsTimer + 0.125 : 0;
986+
indicatorTimer = indicatorTimer < frames.length ? indicatorTimer + 0.125 : 0;
922987
}, delay);
923988
};
924989

@@ -933,7 +998,11 @@ export const spinner = () => {
933998
? color.red(S_STEP_CANCEL)
934999
: color.red(S_STEP_ERROR);
9351000
_message = parseMessage(msg ?? _message);
936-
process.stdout.write(`${step} ${_message}\n`);
1001+
if (indicator === 'timer') {
1002+
process.stdout.write(`${step} ${_message} ${formatTimer(_origin)}\n`);
1003+
} else {
1004+
process.stdout.write(`${step} ${_message}\n`);
1005+
}
9371006
clearHooks();
9381007
unblock();
9391008
};

0 commit comments

Comments
 (0)