Skip to content

Commit 121a3ee

Browse files
committed
feat(core+prompts): Refactoring of the append renderer
1 parent 63037c1 commit 121a3ee

File tree

5 files changed

+78
-24
lines changed

5 files changed

+78
-24
lines changed

packages/core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"test": "vitest run"
5151
},
5252
"dependencies": {
53+
"@macfja/ansi": "^1.0.0",
5354
"picocolors": "^1.0.0",
5455
"sisteransi": "^1.0.5"
5556
},

packages/core/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,5 +9,5 @@ export { default as Prompt } from './prompts/prompt.js';
99
export { default as SelectPrompt } from './prompts/select.js';
1010
export { default as SelectKeyPrompt } from './prompts/select-key.js';
1111
export { default as TextPrompt } from './prompts/text.js';
12-
export { block, isCancel, getColumns, frameRenderer } from './utils/index.js';
12+
export { block, isCancel, getColumns, frameRenderer, appendRenderer } from './utils/index.js';
1313
export { updateSettings, settings } from './utils/settings.js';

packages/core/src/utils/display.ts

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import type { Writable } from 'node:stream';
2+
import { stripAnsi, wrap as wrapAnsi } from '@macfja/ansi';
23
import { cursor, erase } from 'sisteransi';
34
import wrap from 'wrap-ansi';
45
import { getColumns } from './index.js';
@@ -47,9 +48,47 @@ function renderFrame(newFrame: string, prevFrame: string, output: Writable): str
4748
return frame;
4849
}
4950

51+
/**
52+
* Create a function to render a frame base on the previous call (don't redraw lines that didn't change between 2 calls).
53+
*
54+
* @param output The Writable where to render
55+
* @return The rendering function to call with the new frame to display
56+
*/
5057
export function frameRenderer(output: Writable): (frame: string) => void {
5158
let prevFrame = '';
5259
return (frame: string) => {
5360
prevFrame = renderFrame(frame, prevFrame, output);
5461
};
5562
}
63+
64+
/**
65+
* Create a function to render the next part of a sentence.
66+
* It will automatically wrap (without, if possible, breaking word).
67+
*
68+
* @param output The Writable where to render
69+
* @param joiner The prefix to put in front of each lines
70+
* @param removeLeadingSpace if `true` leading space of new lines will be removed
71+
* @return The rendering function to call with the next part of the content
72+
*/
73+
export function appendRenderer(
74+
output: Writable,
75+
joiner: string,
76+
removeLeadingSpace = true
77+
): (next: string) => void {
78+
let lastLine = joiner;
79+
const joinerLength = stripAnsi(joiner).length + 1;
80+
const newLineRE = removeLeadingSpace ? /\n */g : /\n/g;
81+
82+
return (next: string) => {
83+
const width = getColumns(output) - joinerLength;
84+
const lines =
85+
lastLine.substring(0, joiner.length) +
86+
wrapAnsi(`${lastLine.substring(joiner.length)}${next}`, width).replace(
87+
newLineRE,
88+
`\n${joiner}`
89+
);
90+
output?.write(cursor.move(-999, 0) + erase.lines(1));
91+
output?.write(lines);
92+
lastLine = lines.substring(Math.max(0, lines.lastIndexOf('\n') + 1));
93+
};
94+
}

packages/prompts/src/stream.ts

Lines changed: 7 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,21 @@
1-
import { stripVTControlCharacters as strip } from 'node:util';
1+
import { appendRenderer } from '@clack/core';
22
import color from 'picocolors';
33
import { S_BAR, S_ERROR, S_INFO, S_STEP_SUBMIT, S_SUCCESS, S_WARN } from './common.js';
44
import type { LogMessageOptions } from './log.js';
55

66
const prefix = `${color.gray(S_BAR)} `;
77

8-
// TODO (43081j): this currently doesn't support custom `output` writables
9-
// because we rely on `columns` existing (i.e. `process.stdout.columns).
10-
//
11-
// If we want to support `output` being passed in, we will need to use
12-
// a condition like `if (output insance Writable)` to check if it has columns
138
export const stream = {
149
message: async (
1510
iterable: Iterable<string> | AsyncIterable<string>,
16-
{ symbol = color.gray(S_BAR) }: LogMessageOptions = {}
11+
{ symbol = color.gray(S_BAR), output = process.stdout }: LogMessageOptions = {}
1712
) => {
18-
process.stdout.write(`${color.gray(S_BAR)}\n${symbol} `);
19-
let lineWidth = 3;
20-
for await (let chunk of iterable) {
21-
chunk = chunk.replace(/\n/g, `\n${prefix}`);
22-
if (chunk.includes('\n')) {
23-
lineWidth = 3 + strip(chunk.slice(chunk.lastIndexOf('\n'))).length;
24-
}
25-
const chunkLen = strip(chunk).length;
26-
if (lineWidth + chunkLen < process.stdout.columns) {
27-
lineWidth += chunkLen;
28-
process.stdout.write(chunk);
29-
} else {
30-
process.stdout.write(`\n${prefix}${chunk.trimStart()}`);
31-
lineWidth = 3 + strip(chunk.trimStart()).length;
32-
}
13+
output.write(`${color.gray(S_BAR)}\n${symbol} `);
14+
const renderer = appendRenderer(output, prefix);
15+
for await (const chunk of iterable) {
16+
renderer(chunk);
3317
}
34-
process.stdout.write('\n');
18+
output.write('\n');
3519
},
3620
info: (iterable: Iterable<string> | AsyncIterable<string>) => {
3721
return stream.message(iterable, { symbol: color.blue(S_INFO) });

pnpm-lock.yaml

Lines changed: 30 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)