effective-progress 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -8
- package/package.json +8 -5
- package/src/console.ts +130 -38
- package/src/renderer.ts +45 -53
- package/src/runtime.ts +14 -44
- package/src/types.ts +0 -3
package/README.md
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
- multiple nested tree-like progress bars
|
|
17
17
|
- spinner support for “we have no idea how long this takes” work
|
|
18
|
-
-
|
|
18
|
+
- keep using `Console.log` / `Effect.logInfo` while raw console calls are buffered and replayed through your existing `Console` / logger between frame renders
|
|
19
19
|
- familiar `.all` and `.forEach` APIs — swap `Effect` for `Progress`, get progress bars basically for free
|
|
20
20
|
- flicker-free rendering (in theory) by drawing everything in a single terminal frame
|
|
21
21
|
|
|
@@ -88,11 +88,11 @@ Effect.runPromise(program);
|
|
|
88
88
|
|
|
89
89
|
## Configuration
|
|
90
90
|
|
|
91
|
-
###
|
|
91
|
+
### Console replay
|
|
92
92
|
|
|
93
|
-
- `
|
|
94
|
-
-
|
|
95
|
-
-
|
|
93
|
+
- `Progress.task`, `Progress.all`, and `Progress.forEach` buffer `Console.log`/`Console.dir` calls and replay them through the outer `Console`.
|
|
94
|
+
- Calls are replayed between progress frame renders to avoid tearing the TTY frame.
|
|
95
|
+
- Formatting is controlled by the API consumer's logger/console implementation.
|
|
96
96
|
|
|
97
97
|
### Configuring renderer and progress bars
|
|
98
98
|
|
|
@@ -111,7 +111,6 @@ import * as Progress from "effective-progress";
|
|
|
111
111
|
const configured = program.pipe(
|
|
112
112
|
Effect.provideService(Progress.RendererConfig, {
|
|
113
113
|
width: 80,
|
|
114
|
-
maxLogLines: 12,
|
|
115
114
|
nonTtyUpdateStep: 2,
|
|
116
115
|
}),
|
|
117
116
|
Effect.provideService(Progress.ProgressBarConfig, {
|
|
@@ -180,13 +179,13 @@ yield *
|
|
|
180
179
|
|
|
181
180
|
## Manual task control
|
|
182
181
|
|
|
183
|
-
For manual usage, `task`
|
|
182
|
+
For manual usage, `task` still provides the current `Task` context, while logs continue through your outer `Console`:
|
|
184
183
|
|
|
185
184
|
```ts
|
|
186
185
|
const program = Progress.task(
|
|
187
186
|
Effect.gen(function* () {
|
|
188
187
|
const currentTask = yield* Progress.Task;
|
|
189
|
-
yield* Console.log("This log is
|
|
188
|
+
yield* Console.log("This log is handled by the outer Console", { taskId: currentTask });
|
|
190
189
|
yield* Effect.sleep("1 second");
|
|
191
190
|
}),
|
|
192
191
|
{ description: "Manual task" },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effective-progress",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"description": "Effect-first terminal progress bars with nested multibar support",
|
|
5
5
|
"homepage": "https://github.com/stromseng/effective-progress#readme",
|
|
6
6
|
"bugs": {
|
|
@@ -19,6 +19,9 @@
|
|
|
19
19
|
],
|
|
20
20
|
"type": "module",
|
|
21
21
|
"module": "index.ts",
|
|
22
|
+
"engines": {
|
|
23
|
+
"node": ">=20.12.0"
|
|
24
|
+
},
|
|
22
25
|
"publishConfig": {
|
|
23
26
|
"access": "public"
|
|
24
27
|
},
|
|
@@ -30,18 +33,18 @@
|
|
|
30
33
|
"format:check": "oxfmt --check ."
|
|
31
34
|
},
|
|
32
35
|
"dependencies": {
|
|
33
|
-
"chalk": "^5.6.2",
|
|
34
|
-
"effect": "^3.19.17",
|
|
35
36
|
"es-toolkit": "^1.44.0",
|
|
36
37
|
"type-fest": "^5.4.4"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@effect/language-service": "^0.73.1",
|
|
40
41
|
"@types/bun": "latest",
|
|
42
|
+
"effect": "^3.19.17",
|
|
41
43
|
"oxfmt": "^0.32.0",
|
|
42
|
-
"oxlint": "^1.47.0"
|
|
44
|
+
"oxlint": "^1.47.0",
|
|
45
|
+
"typescript": "^5"
|
|
43
46
|
},
|
|
44
47
|
"peerDependencies": {
|
|
45
|
-
"
|
|
48
|
+
"effect": "^3.19.17"
|
|
46
49
|
}
|
|
47
50
|
}
|
package/src/console.ts
CHANGED
|
@@ -1,85 +1,177 @@
|
|
|
1
|
-
import { Console, Effect } from "effect";
|
|
2
|
-
import { formatWithOptions } from "node:util";
|
|
1
|
+
import { Console, Effect, Ref } from "effect";
|
|
3
2
|
|
|
4
|
-
export
|
|
5
|
-
|
|
3
|
+
export type BufferedConsoleMethod =
|
|
4
|
+
| "assert"
|
|
5
|
+
| "debug"
|
|
6
|
+
| "dir"
|
|
7
|
+
| "dirxml"
|
|
8
|
+
| "error"
|
|
9
|
+
| "group"
|
|
10
|
+
| "groupCollapsed"
|
|
11
|
+
| "groupEnd"
|
|
12
|
+
| "info"
|
|
13
|
+
| "log"
|
|
14
|
+
| "table"
|
|
15
|
+
| "trace"
|
|
16
|
+
| "warn";
|
|
17
|
+
|
|
18
|
+
export interface BufferedConsoleCall {
|
|
19
|
+
readonly method: BufferedConsoleMethod;
|
|
20
|
+
readonly args: ReadonlyArray<unknown>;
|
|
21
|
+
readonly unsafe: boolean;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface ConsoleBridge {
|
|
25
|
+
readonly progressConsole: Console.Console;
|
|
26
|
+
readonly appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>;
|
|
27
|
+
readonly replayLogs: (
|
|
28
|
+
logs: ReadonlyArray<BufferedConsoleCall>,
|
|
29
|
+
) => Effect.Effect<void, never, never>;
|
|
30
|
+
readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void, never, never>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const replayBufferedConsoleCall = (
|
|
6
34
|
outerConsole: Console.Console,
|
|
35
|
+
call: BufferedConsoleCall,
|
|
36
|
+
): Effect.Effect<void, never, never> => {
|
|
37
|
+
type DirOptions = Parameters<Console.Console["dir"]>[1];
|
|
38
|
+
type GroupOptions = Parameters<Console.Console["group"]>[0];
|
|
39
|
+
|
|
40
|
+
switch (call.method) {
|
|
41
|
+
case "assert": {
|
|
42
|
+
const [condition, ...rest] = call.args;
|
|
43
|
+
return outerConsole.assert(condition as boolean, ...rest);
|
|
44
|
+
}
|
|
45
|
+
case "debug":
|
|
46
|
+
return outerConsole.debug(...call.args);
|
|
47
|
+
case "dir":
|
|
48
|
+
return outerConsole.dir(call.args[0], call.args[1] as DirOptions);
|
|
49
|
+
case "dirxml":
|
|
50
|
+
return outerConsole.dirxml(...call.args);
|
|
51
|
+
case "error":
|
|
52
|
+
return outerConsole.error(...call.args);
|
|
53
|
+
case "group":
|
|
54
|
+
return outerConsole.group(call.args[0] as GroupOptions);
|
|
55
|
+
case "groupCollapsed":
|
|
56
|
+
return outerConsole.group(call.args[0] as GroupOptions);
|
|
57
|
+
case "groupEnd":
|
|
58
|
+
return outerConsole.groupEnd;
|
|
59
|
+
case "info":
|
|
60
|
+
return outerConsole.info(...call.args);
|
|
61
|
+
case "log":
|
|
62
|
+
return outerConsole.log(...call.args);
|
|
63
|
+
case "table":
|
|
64
|
+
return outerConsole.table(call.args[0], call.args[1] as ReadonlyArray<string> | undefined);
|
|
65
|
+
case "trace":
|
|
66
|
+
return outerConsole.trace(...call.args);
|
|
67
|
+
case "warn":
|
|
68
|
+
return outerConsole.warn(...call.args);
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export const makeProgressConsole = (
|
|
73
|
+
appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>,
|
|
7
74
|
): Console.Console => {
|
|
8
|
-
const log = (...args: ReadonlyArray<unknown>) =>
|
|
9
|
-
|
|
10
|
-
|
|
75
|
+
const log = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) =>
|
|
76
|
+
appendLog({ method, args, unsafe: false });
|
|
77
|
+
const unsafeLog = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) => {
|
|
78
|
+
Effect.runSync(appendLog({ method, args, unsafe: true }));
|
|
11
79
|
};
|
|
12
80
|
|
|
13
|
-
const delegate = (effect: Effect.Effect<void, never, never>) => effect;
|
|
14
|
-
|
|
15
81
|
return Console.Console.of({
|
|
16
82
|
[Console.TypeId]: Console.TypeId,
|
|
17
|
-
assert(condition, ...args)
|
|
18
|
-
return condition ? Effect.void : log("Assertion failed:", ...args);
|
|
19
|
-
},
|
|
83
|
+
assert: (condition, ...args) => log("assert", condition, ...args),
|
|
20
84
|
clear: Effect.void,
|
|
21
85
|
count: (_label) => Effect.void,
|
|
22
86
|
countReset: (_label) => Effect.void,
|
|
23
|
-
debug: (...args) => log(...args),
|
|
24
|
-
dir: (item, options) => log(
|
|
25
|
-
dirxml: (...args) => log(...args),
|
|
26
|
-
error: (...args) => log(...args),
|
|
27
|
-
group: (...args) => log(...args),
|
|
28
|
-
groupEnd:
|
|
29
|
-
info: (...args) => log(...args),
|
|
30
|
-
log: (...args) => log(...args),
|
|
31
|
-
table: (tabularData, properties) => log(tabularData, properties),
|
|
87
|
+
debug: (...args) => log("debug", ...args),
|
|
88
|
+
dir: (item, options) => log("dir", item, options),
|
|
89
|
+
dirxml: (...args) => log("dirxml", ...args),
|
|
90
|
+
error: (...args) => log("error", ...args),
|
|
91
|
+
group: (...args) => log("group", ...args),
|
|
92
|
+
groupEnd: log("groupEnd"),
|
|
93
|
+
info: (...args) => log("info", ...args),
|
|
94
|
+
log: (...args) => log("log", ...args),
|
|
95
|
+
table: (tabularData, properties) => log("table", tabularData, properties),
|
|
32
96
|
time: (_label) => Effect.void,
|
|
33
97
|
timeEnd: (_label) => Effect.void,
|
|
34
|
-
timeLog: (_label, ...args) => log(...args),
|
|
35
|
-
trace: (...args) =>
|
|
36
|
-
warn: (...args) => log(...args),
|
|
98
|
+
timeLog: (_label, ...args) => log("info", ...args),
|
|
99
|
+
trace: (...args) => log("trace", ...args),
|
|
100
|
+
warn: (...args) => log("warn", ...args),
|
|
37
101
|
unsafe: {
|
|
38
102
|
assert(condition, ...args) {
|
|
39
|
-
|
|
103
|
+
unsafeLog("assert", condition, ...args);
|
|
40
104
|
},
|
|
41
105
|
clear() {},
|
|
42
106
|
count(_label) {},
|
|
43
107
|
countReset(_label) {},
|
|
44
108
|
debug(...args) {
|
|
45
|
-
unsafeLog(...args);
|
|
109
|
+
unsafeLog("debug", ...args);
|
|
46
110
|
},
|
|
47
111
|
dir(item, options) {
|
|
48
|
-
unsafeLog(
|
|
112
|
+
unsafeLog("dir", item, options);
|
|
49
113
|
},
|
|
50
114
|
dirxml(...args) {
|
|
51
|
-
unsafeLog(...args);
|
|
115
|
+
unsafeLog("dirxml", ...args);
|
|
52
116
|
},
|
|
53
117
|
error(...args) {
|
|
54
|
-
unsafeLog(...args);
|
|
118
|
+
unsafeLog("error", ...args);
|
|
55
119
|
},
|
|
56
120
|
group(...args) {
|
|
57
|
-
unsafeLog(...args);
|
|
121
|
+
unsafeLog("group", ...args);
|
|
58
122
|
},
|
|
59
123
|
groupCollapsed(...args) {
|
|
60
|
-
unsafeLog(...args);
|
|
124
|
+
unsafeLog("groupCollapsed", ...args);
|
|
125
|
+
},
|
|
126
|
+
groupEnd() {
|
|
127
|
+
unsafeLog("groupEnd");
|
|
61
128
|
},
|
|
62
|
-
groupEnd() {},
|
|
63
129
|
info(...args) {
|
|
64
|
-
unsafeLog(...args);
|
|
130
|
+
unsafeLog("info", ...args);
|
|
65
131
|
},
|
|
66
132
|
log(...args) {
|
|
67
|
-
unsafeLog(...args);
|
|
133
|
+
unsafeLog("log", ...args);
|
|
68
134
|
},
|
|
69
135
|
table(tabularData, properties) {
|
|
70
|
-
unsafeLog(tabularData, properties);
|
|
136
|
+
unsafeLog("table", tabularData, properties);
|
|
71
137
|
},
|
|
72
138
|
time(_label) {},
|
|
73
139
|
timeEnd(_label) {},
|
|
74
140
|
timeLog(_label, ...args) {
|
|
75
|
-
unsafeLog(...args);
|
|
141
|
+
unsafeLog("info", ...args);
|
|
76
142
|
},
|
|
77
143
|
trace(...args) {
|
|
78
|
-
|
|
144
|
+
unsafeLog("trace", ...args);
|
|
79
145
|
},
|
|
80
146
|
warn(...args) {
|
|
81
|
-
unsafeLog(...args);
|
|
147
|
+
unsafeLog("warn", ...args);
|
|
82
148
|
},
|
|
83
149
|
},
|
|
84
150
|
});
|
|
85
151
|
};
|
|
152
|
+
|
|
153
|
+
export const makeConsoleBridge = (
|
|
154
|
+
outerConsole: Console.Console,
|
|
155
|
+
pendingLogsRef: Ref.Ref<ReadonlyArray<BufferedConsoleCall>>,
|
|
156
|
+
markDirty: Effect.Effect<void, never, never>,
|
|
157
|
+
): ConsoleBridge => {
|
|
158
|
+
const appendLog = (call: BufferedConsoleCall) =>
|
|
159
|
+
call.args.length === 0
|
|
160
|
+
? Effect.void
|
|
161
|
+
: Ref.update(pendingLogsRef, (logs) => [...logs, call]).pipe(Effect.zipRight(markDirty));
|
|
162
|
+
|
|
163
|
+
const replayLogs = (logs: ReadonlyArray<BufferedConsoleCall>) =>
|
|
164
|
+
Effect.forEach(logs, (call) => replayBufferedConsoleCall(outerConsole, call), {
|
|
165
|
+
discard: true,
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const log = (...args: ReadonlyArray<unknown>) =>
|
|
169
|
+
args.length === 0 ? Effect.void : appendLog({ method: "log", args, unsafe: false });
|
|
170
|
+
|
|
171
|
+
return {
|
|
172
|
+
progressConsole: makeProgressConsole(appendLog),
|
|
173
|
+
appendLog,
|
|
174
|
+
replayLogs,
|
|
175
|
+
log,
|
|
176
|
+
};
|
|
177
|
+
};
|
package/src/renderer.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { styleText } from "node:util";
|
|
2
2
|
import { Clock, Context, Effect, Layer, Ref } from "effect";
|
|
3
3
|
import { fitRenderedText, visibleWidth } from "./renderer/ansi";
|
|
4
4
|
import { computeTreeInfo, renderTreePrefix } from "./renderer/tree";
|
|
@@ -81,8 +81,13 @@ const formatEta = (snapshot: TaskSnapshot, now: number): string => {
|
|
|
81
81
|
const reserveTimeWidth = (formattedDuration: string): string =>
|
|
82
82
|
formattedDuration.padStart(RESERVED_SECONDS_WIDTH, " ");
|
|
83
83
|
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
type StyleFormat = Parameters<typeof styleText>[0];
|
|
85
|
+
|
|
86
|
+
const applyStyle = (format: StyleFormat, text: string): string =>
|
|
87
|
+
styleText(format, text, { validateStream: false });
|
|
88
|
+
|
|
89
|
+
const styleIfTTY = (isTTY: boolean, format: StyleFormat, text: string): string =>
|
|
90
|
+
isTTY ? applyStyle(format, text) : text;
|
|
86
91
|
|
|
87
92
|
interface ColumnBaseOptions {
|
|
88
93
|
readonly id?: string;
|
|
@@ -220,19 +225,19 @@ export class BarColumn implements ProgressColumn {
|
|
|
220
225
|
const fill = config.fillChar.repeat(filled);
|
|
221
226
|
const empty = config.emptyChar.repeat(Math.max(0, innerWidth - filled));
|
|
222
227
|
|
|
223
|
-
const fillStyle =
|
|
228
|
+
const fillStyle: StyleFormat =
|
|
224
229
|
context.task.status === "failed"
|
|
225
|
-
?
|
|
230
|
+
? "red"
|
|
226
231
|
: context.task.status === "done"
|
|
227
|
-
?
|
|
228
|
-
:
|
|
229
|
-
const emptyStyle = context.task.status === "failed" ?
|
|
232
|
+
? "green"
|
|
233
|
+
: "blue";
|
|
234
|
+
const emptyStyle: StyleFormat = context.task.status === "failed" ? "red" : ["white", "dim"];
|
|
230
235
|
|
|
231
236
|
return [
|
|
232
|
-
styleIfTTY(context.isTTY,
|
|
237
|
+
styleIfTTY(context.isTTY, ["white", "dim"], config.leftBracket),
|
|
233
238
|
styleIfTTY(context.isTTY, fillStyle, fill),
|
|
234
239
|
styleIfTTY(context.isTTY, emptyStyle, empty),
|
|
235
|
-
styleIfTTY(context.isTTY,
|
|
240
|
+
styleIfTTY(context.isTTY, ["white", "dim"], config.rightBracket),
|
|
236
241
|
].join("");
|
|
237
242
|
}
|
|
238
243
|
|
|
@@ -299,7 +304,7 @@ export class AmountColumn implements ProgressColumn {
|
|
|
299
304
|
if (task.units._tag === "DeterminateTaskUnits") {
|
|
300
305
|
return styleIfTTY(
|
|
301
306
|
context.isTTY,
|
|
302
|
-
task.status === "failed" ?
|
|
307
|
+
task.status === "failed" ? "red" : "whiteBright",
|
|
303
308
|
formatDeterminateUnits(task.units.completed, task.units.total),
|
|
304
309
|
);
|
|
305
310
|
}
|
|
@@ -308,14 +313,14 @@ export class AmountColumn implements ProgressColumn {
|
|
|
308
313
|
const frames = task.config.spinnerFrames;
|
|
309
314
|
const frameIndex = (task.units.spinnerFrame + context.tick) % frames.length;
|
|
310
315
|
const frame = frames[frameIndex] ?? frames[0] ?? "";
|
|
311
|
-
return styleIfTTY(context.isTTY,
|
|
316
|
+
return styleIfTTY(context.isTTY, "yellow", frame);
|
|
312
317
|
}
|
|
313
318
|
|
|
314
319
|
if (task.status === "done") {
|
|
315
|
-
return styleIfTTY(context.isTTY,
|
|
320
|
+
return styleIfTTY(context.isTTY, "green", this.doneSymbol);
|
|
316
321
|
}
|
|
317
322
|
|
|
318
|
-
return styleIfTTY(context.isTTY,
|
|
323
|
+
return styleIfTTY(context.isTTY, "red", this.failedSymbol);
|
|
319
324
|
}
|
|
320
325
|
}
|
|
321
326
|
|
|
@@ -353,7 +358,7 @@ export class ElapsedColumn implements ProgressColumn {
|
|
|
353
358
|
render(context: ProgressColumnContext): string {
|
|
354
359
|
const raw = formatElapsed(context.task, context.now);
|
|
355
360
|
const elapsed = this.padSeconds ? reserveTimeWidth(raw) : raw;
|
|
356
|
-
return styleIfTTY(context.isTTY,
|
|
361
|
+
return styleIfTTY(context.isTTY, "gray", elapsed);
|
|
357
362
|
}
|
|
358
363
|
}
|
|
359
364
|
|
|
@@ -421,7 +426,7 @@ export class EtaColumn implements ProgressColumn {
|
|
|
421
426
|
}
|
|
422
427
|
|
|
423
428
|
render(context: ProgressColumnContext): string {
|
|
424
|
-
return styleIfTTY(context.isTTY,
|
|
429
|
+
return styleIfTTY(context.isTTY, "gray", this.resolveEtaText(context, true));
|
|
425
430
|
}
|
|
426
431
|
|
|
427
432
|
variants(context: ProgressColumnContext): ReadonlyArray<ProgressColumnVariant> {
|
|
@@ -438,17 +443,17 @@ export class EtaColumn implements ProgressColumn {
|
|
|
438
443
|
if (compact === full) {
|
|
439
444
|
return [
|
|
440
445
|
{
|
|
441
|
-
render: () => styleIfTTY(context.isTTY,
|
|
446
|
+
render: () => styleIfTTY(context.isTTY, "gray", full),
|
|
442
447
|
},
|
|
443
448
|
];
|
|
444
449
|
}
|
|
445
450
|
|
|
446
451
|
return [
|
|
447
452
|
{
|
|
448
|
-
render: () => styleIfTTY(context.isTTY,
|
|
453
|
+
render: () => styleIfTTY(context.isTTY, "gray", full),
|
|
449
454
|
},
|
|
450
455
|
{
|
|
451
|
-
render: () => styleIfTTY(context.isTTY,
|
|
456
|
+
render: () => styleIfTTY(context.isTTY, "gray", compact),
|
|
452
457
|
},
|
|
453
458
|
];
|
|
454
459
|
}
|
|
@@ -1002,31 +1007,20 @@ const renderTaskFrame = (
|
|
|
1002
1007
|
};
|
|
1003
1008
|
|
|
1004
1009
|
export interface FrameRendererService {
|
|
1005
|
-
readonly run: (
|
|
1010
|
+
readonly run: <LogEntry>(
|
|
1006
1011
|
storeRef: Ref.Ref<TaskStore>,
|
|
1007
|
-
|
|
1008
|
-
|
|
1012
|
+
pendingLogsRef: Ref.Ref<ReadonlyArray<LogEntry>>,
|
|
1013
|
+
replayLogs: (logs: ReadonlyArray<LogEntry>) => Effect.Effect<void, never, never>,
|
|
1009
1014
|
dirtyRef: Ref.Ref<boolean>,
|
|
1010
1015
|
terminal: ProgressTerminalService,
|
|
1011
1016
|
isTTY: boolean,
|
|
1012
1017
|
rendererConfig: RendererConfigShape,
|
|
1013
|
-
maxRetainedLogLines: number,
|
|
1014
1018
|
) => Effect.Effect<void>;
|
|
1015
1019
|
}
|
|
1016
1020
|
|
|
1017
1021
|
const makeDefaultFrameRenderer = (): FrameRendererService => ({
|
|
1018
|
-
run: (
|
|
1019
|
-
storeRef,
|
|
1020
|
-
logsRef,
|
|
1021
|
-
pendingLogsRef,
|
|
1022
|
-
dirtyRef,
|
|
1023
|
-
terminal,
|
|
1024
|
-
isTTY,
|
|
1025
|
-
rendererConfig,
|
|
1026
|
-
maxRetainedLogLines,
|
|
1027
|
-
) =>
|
|
1022
|
+
run: (storeRef, pendingLogsRef, replayLogs, dirtyRef, terminal, isTTY, rendererConfig) =>
|
|
1028
1023
|
Effect.gen(function* () {
|
|
1029
|
-
const retainLogHistory = maxRetainedLogLines > 0;
|
|
1030
1024
|
const compiledColumns = normalizeColumns(rendererConfig.columns);
|
|
1031
1025
|
let previousLineCount = 0;
|
|
1032
1026
|
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
@@ -1152,33 +1146,27 @@ const makeDefaultFrameRenderer = (): FrameRendererService => ({
|
|
|
1152
1146
|
for (let i = 1; i < previousLineCount; i++) {
|
|
1153
1147
|
frame += MOVE_UP_ONE + CLEAR_LINE;
|
|
1154
1148
|
}
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
if (retainLogHistory) {
|
|
1158
|
-
const historyLogs = yield* Ref.get(logsRef);
|
|
1159
|
-
const lines = yield* clipTTYFrameLines([...historyLogs, ...renderedFrame.lines]);
|
|
1160
|
-
if (lines.length > 0) {
|
|
1161
|
-
frame += lines.join("\n");
|
|
1162
|
-
}
|
|
1163
|
-
previousLineCount = lines.length;
|
|
1164
|
-
} else {
|
|
1165
|
-
if (drainedLogs.length > 0) {
|
|
1166
|
-
frame += `${drainedLogs.join("\n")}\n`;
|
|
1167
|
-
}
|
|
1168
|
-
if (renderedFrame.lines.length > 0) {
|
|
1169
|
-
frame += renderedFrame.lines.join("\n");
|
|
1170
|
-
}
|
|
1171
|
-
previousLineCount = renderedFrame.lines.length;
|
|
1149
|
+
previousLineCount = 0;
|
|
1172
1150
|
}
|
|
1173
1151
|
|
|
1174
1152
|
if (frame) {
|
|
1175
1153
|
yield* terminal.writeStderr(frame);
|
|
1176
1154
|
}
|
|
1155
|
+
|
|
1156
|
+
if (drainedLogs.length > 0) {
|
|
1157
|
+
yield* replayLogs(drainedLogs);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
const lines = yield* clipTTYFrameLines(renderedFrame.lines);
|
|
1161
|
+
if (lines.length > 0) {
|
|
1162
|
+
yield* terminal.writeStderr(lines.join("\n"));
|
|
1163
|
+
}
|
|
1164
|
+
previousLineCount = lines.length;
|
|
1177
1165
|
return;
|
|
1178
1166
|
}
|
|
1179
1167
|
|
|
1180
1168
|
if (drainedLogs.length > 0) {
|
|
1181
|
-
yield*
|
|
1169
|
+
yield* replayLogs(drainedLogs);
|
|
1182
1170
|
}
|
|
1183
1171
|
|
|
1184
1172
|
const orderedForNonTTY = orderedTasks.map((task) => ({
|
|
@@ -1208,7 +1196,7 @@ const makeDefaultFrameRenderer = (): FrameRendererService => ({
|
|
|
1208
1196
|
if (dirty || hasActiveSpinners || hasPendingLogs) {
|
|
1209
1197
|
yield* renderFrame("tick");
|
|
1210
1198
|
}
|
|
1211
|
-
} else if (dirty || hasActiveSpinners) {
|
|
1199
|
+
} else if (dirty || hasActiveSpinners || hasPendingLogs) {
|
|
1212
1200
|
yield* renderFrame("tick");
|
|
1213
1201
|
}
|
|
1214
1202
|
|
|
@@ -1219,6 +1207,10 @@ const makeDefaultFrameRenderer = (): FrameRendererService => ({
|
|
|
1219
1207
|
Effect.ensuring(
|
|
1220
1208
|
Effect.gen(function* () {
|
|
1221
1209
|
if (!rendererActive) {
|
|
1210
|
+
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
1211
|
+
if (drainedLogs.length > 0) {
|
|
1212
|
+
yield* replayLogs(drainedLogs);
|
|
1213
|
+
}
|
|
1222
1214
|
return;
|
|
1223
1215
|
}
|
|
1224
1216
|
|
package/src/runtime.ts
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
3
|
import { mergeWith } from "es-toolkit/object";
|
|
4
|
-
import { formatWithOptions } from "node:util";
|
|
5
4
|
import type { PartialDeep } from "type-fest";
|
|
6
|
-
import {
|
|
5
|
+
import { type BufferedConsoleCall, makeConsoleBridge } from "./console";
|
|
7
6
|
import { Columns, FrameRenderer } from "./renderer";
|
|
8
7
|
import { ProgressTerminal } from "./terminal";
|
|
9
8
|
import type {
|
|
@@ -157,35 +156,40 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
157
156
|
|
|
158
157
|
const terminal = yield* ProgressTerminal;
|
|
159
158
|
const frameRenderer = yield* FrameRenderer;
|
|
159
|
+
const outerConsole = yield* Effect.console;
|
|
160
160
|
const isTTY = yield* terminal.isTTY;
|
|
161
|
-
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
162
161
|
|
|
163
162
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
164
163
|
const storeRef = yield* Ref.make<TaskStore>({
|
|
165
164
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
166
165
|
renderOrder: [],
|
|
167
166
|
});
|
|
168
|
-
const
|
|
169
|
-
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
167
|
+
const pendingLogsRef = yield* Ref.make<ReadonlyArray<BufferedConsoleCall>>([]);
|
|
170
168
|
const dirtyRef = yield* Ref.make(true);
|
|
171
169
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
172
170
|
const scope = yield* Effect.scope;
|
|
173
171
|
|
|
172
|
+
const markDirty = Ref.set(dirtyRef, true);
|
|
173
|
+
const { replayLogs, progressConsole, log } = makeConsoleBridge(
|
|
174
|
+
outerConsole,
|
|
175
|
+
pendingLogsRef,
|
|
176
|
+
markDirty,
|
|
177
|
+
);
|
|
178
|
+
|
|
174
179
|
yield* Effect.forkIn(
|
|
175
180
|
frameRenderer.run(
|
|
176
181
|
storeRef,
|
|
177
|
-
logsRef,
|
|
178
182
|
pendingLogsRef,
|
|
183
|
+
replayLogs,
|
|
179
184
|
dirtyRef,
|
|
180
185
|
terminal,
|
|
181
186
|
isTTY,
|
|
182
187
|
rendererConfig,
|
|
183
|
-
maxRetainedLogLines,
|
|
184
188
|
),
|
|
185
189
|
scope,
|
|
186
190
|
);
|
|
187
|
-
|
|
188
|
-
|
|
191
|
+
// Let the renderer fiber start so queued logs are reliably flushed on scope teardown.
|
|
192
|
+
yield* Effect.sleep("0 millis");
|
|
189
193
|
|
|
190
194
|
const addTask = (options: AddTaskOptions) =>
|
|
191
195
|
Effect.gen(function* () {
|
|
@@ -377,36 +381,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
377
381
|
yield* markDirty;
|
|
378
382
|
});
|
|
379
383
|
|
|
380
|
-
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
381
|
-
Effect.gen(function* () {
|
|
382
|
-
if (args.length === 0) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const message = formatWithOptions(
|
|
387
|
-
{
|
|
388
|
-
colors: isTTY,
|
|
389
|
-
depth: 6,
|
|
390
|
-
},
|
|
391
|
-
...args,
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
|
|
395
|
-
if (maxRetainedLogLines > 0) {
|
|
396
|
-
yield* Ref.update(logsRef, (logs) => {
|
|
397
|
-
const next = [...logs, message];
|
|
398
|
-
if (next.length <= maxRetainedLogLines) {
|
|
399
|
-
return next;
|
|
400
|
-
}
|
|
401
|
-
return next.slice(next.length - maxRetainedLogLines);
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
yield* markDirty;
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
|
|
409
|
-
|
|
410
384
|
const getTask = (taskId: TaskId) =>
|
|
411
385
|
Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
|
|
412
386
|
|
|
@@ -416,7 +390,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
416
390
|
2,
|
|
417
391
|
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
418
392
|
Effect.gen(function* () {
|
|
419
|
-
const outerConsole = yield* Effect.console;
|
|
420
393
|
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
421
394
|
const resolvedParentId =
|
|
422
395
|
options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
|
|
@@ -428,10 +401,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
428
401
|
});
|
|
429
402
|
|
|
430
403
|
return yield* Effect.locally(
|
|
431
|
-
Effect.withConsole(
|
|
432
|
-
Effect.provideService(effect, Task, taskId),
|
|
433
|
-
makeProgressConsole(log, outerConsole),
|
|
434
|
-
),
|
|
404
|
+
Effect.withConsole(Effect.provideService(effect, Task, taskId), progressConsole),
|
|
435
405
|
currentParentRef,
|
|
436
406
|
Option.some(taskId),
|
|
437
407
|
);
|
package/src/types.ts
CHANGED
|
@@ -5,7 +5,6 @@ import type { ProgressColumn } from "./renderer";
|
|
|
5
5
|
export const RendererConfigSchema = Schema.Struct({
|
|
6
6
|
disableUserInput: Schema.Boolean,
|
|
7
7
|
renderIntervalMillis: Schema.Number,
|
|
8
|
-
maxLogLines: Schema.optional(Schema.Number),
|
|
9
8
|
nonTtyUpdateStep: Schema.Number,
|
|
10
9
|
width: Schema.Union(Schema.Number, Schema.Literal("fullwidth")),
|
|
11
10
|
columnGap: Schema.Number,
|
|
@@ -14,7 +13,6 @@ export const RendererConfigSchema = Schema.Struct({
|
|
|
14
13
|
export type RendererConfigShape = {
|
|
15
14
|
readonly disableUserInput: boolean;
|
|
16
15
|
readonly renderIntervalMillis: number;
|
|
17
|
-
readonly maxLogLines?: number;
|
|
18
16
|
readonly nonTtyUpdateStep: number;
|
|
19
17
|
readonly width: number | "fullwidth";
|
|
20
18
|
readonly columnGap: number;
|
|
@@ -46,7 +44,6 @@ export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarC
|
|
|
46
44
|
export const defaultRendererConfig: RendererConfigShape = {
|
|
47
45
|
disableUserInput: true,
|
|
48
46
|
renderIntervalMillis: 100, // 10 FPS
|
|
49
|
-
maxLogLines: 0,
|
|
50
47
|
nonTtyUpdateStep: 5,
|
|
51
48
|
width: 120,
|
|
52
49
|
columnGap: 1,
|