effective-progress 0.4.1 → 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 +93 -71
- package/package.json +8 -5
- package/src/api.ts +57 -21
- package/src/console.ts +130 -38
- package/src/index.ts +0 -1
- package/src/renderer/ansi.ts +138 -0
- package/src/renderer/tree.ts +68 -0
- package/src/renderer/types.ts +67 -0
- package/src/renderer.ts +761 -741
- package/src/runtime.ts +37 -86
- package/src/types.ts +26 -9
- package/src/theme.ts +0 -87
package/src/runtime.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
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 {
|
|
7
|
-
import {
|
|
5
|
+
import { type BufferedConsoleCall, makeConsoleBridge } from "./console";
|
|
6
|
+
import { Columns, FrameRenderer } from "./renderer";
|
|
8
7
|
import { ProgressTerminal } from "./terminal";
|
|
9
|
-
import { Theme, type ThemeService } from "./theme";
|
|
10
8
|
import type {
|
|
11
9
|
AddTaskOptions,
|
|
12
10
|
ProgressService,
|
|
13
11
|
RenderRow,
|
|
12
|
+
RendererConfigShape,
|
|
14
13
|
TaskStore,
|
|
15
14
|
UpdateTaskOptions,
|
|
16
15
|
} from "./types";
|
|
@@ -130,53 +129,67 @@ const removeFromRenderOrder = (
|
|
|
130
129
|
return next;
|
|
131
130
|
};
|
|
132
131
|
|
|
132
|
+
const withDefaultColumns = (rendererConfig: RendererConfigShape): RendererConfigShape => ({
|
|
133
|
+
...rendererConfig,
|
|
134
|
+
columns: rendererConfig.columns.length > 0 ? rendererConfig.columns : Columns.defaults(),
|
|
135
|
+
});
|
|
136
|
+
|
|
133
137
|
const makeProgressService = Effect.gen(function* () {
|
|
134
138
|
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
135
139
|
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
+
|
|
141
|
+
const rendererConfig = withDefaultColumns(
|
|
142
|
+
decodeRendererConfigSync(
|
|
143
|
+
mergeConfig(
|
|
144
|
+
defaultRendererConfig,
|
|
145
|
+
Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
|
|
146
|
+
),
|
|
140
147
|
),
|
|
141
148
|
);
|
|
149
|
+
|
|
142
150
|
const progressBarConfig = decodeProgressBarConfigSync(
|
|
143
151
|
mergeConfig(
|
|
144
152
|
defaultProgressBarConfig,
|
|
145
153
|
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
146
154
|
),
|
|
147
155
|
);
|
|
156
|
+
|
|
148
157
|
const terminal = yield* ProgressTerminal;
|
|
149
158
|
const frameRenderer = yield* FrameRenderer;
|
|
159
|
+
const outerConsole = yield* Effect.console;
|
|
150
160
|
const isTTY = yield* terminal.isTTY;
|
|
151
|
-
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
152
161
|
|
|
153
162
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
154
163
|
const storeRef = yield* Ref.make<TaskStore>({
|
|
155
164
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
156
165
|
renderOrder: [],
|
|
157
|
-
themes: new Map<TaskId, ThemeService>(),
|
|
158
166
|
});
|
|
159
|
-
const
|
|
160
|
-
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
167
|
+
const pendingLogsRef = yield* Ref.make<ReadonlyArray<BufferedConsoleCall>>([]);
|
|
161
168
|
const dirtyRef = yield* Ref.make(true);
|
|
162
169
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
163
170
|
const scope = yield* Effect.scope;
|
|
164
171
|
|
|
172
|
+
const markDirty = Ref.set(dirtyRef, true);
|
|
173
|
+
const { replayLogs, progressConsole, log } = makeConsoleBridge(
|
|
174
|
+
outerConsole,
|
|
175
|
+
pendingLogsRef,
|
|
176
|
+
markDirty,
|
|
177
|
+
);
|
|
178
|
+
|
|
165
179
|
yield* Effect.forkIn(
|
|
166
|
-
frameRenderer.run(
|
|
180
|
+
frameRenderer.run(
|
|
167
181
|
storeRef,
|
|
168
|
-
logsRef,
|
|
169
182
|
pendingLogsRef,
|
|
183
|
+
replayLogs,
|
|
170
184
|
dirtyRef,
|
|
171
185
|
terminal,
|
|
172
186
|
isTTY,
|
|
173
187
|
rendererConfig,
|
|
174
|
-
|
|
175
|
-
}),
|
|
188
|
+
),
|
|
176
189
|
scope,
|
|
177
190
|
);
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
// Let the renderer fiber start so queued logs are reliably flushed on scope teardown.
|
|
192
|
+
yield* Effect.sleep("0 millis");
|
|
180
193
|
|
|
181
194
|
const addTask = (options: AddTaskOptions) =>
|
|
182
195
|
Effect.gen(function* () {
|
|
@@ -184,7 +197,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
184
197
|
options.parentId === undefined
|
|
185
198
|
? yield* FiberRef.get(currentParentRef)
|
|
186
199
|
: Option.some(options.parentId);
|
|
187
|
-
const themeOption = yield* Effect.serviceOption(Theme);
|
|
188
200
|
const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
|
|
189
201
|
const units =
|
|
190
202
|
options.total === undefined || options.total <= 0
|
|
@@ -219,11 +231,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
219
231
|
const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
|
|
220
232
|
const nextOrder = [...s.renderOrder];
|
|
221
233
|
nextOrder.splice(index, 0, { id: taskId, depth });
|
|
222
|
-
|
|
223
|
-
if (Option.isSome(themeOption)) {
|
|
224
|
-
nextThemes.set(taskId, themeOption.value);
|
|
225
|
-
}
|
|
226
|
-
return { tasks: nextTasks, renderOrder: nextOrder, themes: nextThemes };
|
|
234
|
+
return { tasks: nextTasks, renderOrder: nextOrder };
|
|
227
235
|
});
|
|
228
236
|
yield* markDirty;
|
|
229
237
|
|
|
@@ -260,7 +268,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
260
268
|
}
|
|
261
269
|
}
|
|
262
270
|
|
|
263
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder
|
|
271
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
264
272
|
}).pipe(Effect.zipRight(markDirty));
|
|
265
273
|
|
|
266
274
|
const advanceTask = (taskId: TaskId, amount = 1) =>
|
|
@@ -294,7 +302,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
294
302
|
}),
|
|
295
303
|
);
|
|
296
304
|
|
|
297
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder
|
|
305
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
298
306
|
}).pipe(Effect.zipRight(markDirty));
|
|
299
307
|
|
|
300
308
|
const completeTask = (taskId: TaskId) =>
|
|
@@ -307,12 +315,9 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
307
315
|
const nextTasks = new Map(store.tasks);
|
|
308
316
|
if (snapshot.transient) {
|
|
309
317
|
nextTasks.delete(taskId);
|
|
310
|
-
const nextThemes = new Map(store.themes);
|
|
311
|
-
nextThemes.delete(taskId);
|
|
312
318
|
return {
|
|
313
319
|
tasks: nextTasks,
|
|
314
320
|
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
315
|
-
themes: nextThemes,
|
|
316
321
|
};
|
|
317
322
|
}
|
|
318
323
|
|
|
@@ -336,7 +341,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
336
341
|
completedAt: now,
|
|
337
342
|
}),
|
|
338
343
|
);
|
|
339
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder
|
|
344
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
340
345
|
});
|
|
341
346
|
yield* markDirty;
|
|
342
347
|
});
|
|
@@ -351,12 +356,9 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
351
356
|
const nextTasks = new Map(store.tasks);
|
|
352
357
|
if (snapshot.transient) {
|
|
353
358
|
nextTasks.delete(taskId);
|
|
354
|
-
const nextThemes = new Map(store.themes);
|
|
355
|
-
nextThemes.delete(taskId);
|
|
356
359
|
return {
|
|
357
360
|
tasks: nextTasks,
|
|
358
361
|
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
359
|
-
themes: nextThemes,
|
|
360
362
|
};
|
|
361
363
|
}
|
|
362
364
|
|
|
@@ -374,42 +376,11 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
374
376
|
completedAt: now,
|
|
375
377
|
}),
|
|
376
378
|
);
|
|
377
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder
|
|
379
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
378
380
|
});
|
|
379
381
|
yield* markDirty;
|
|
380
382
|
});
|
|
381
383
|
|
|
382
|
-
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
383
|
-
Effect.gen(function* () {
|
|
384
|
-
if (args.length === 0) {
|
|
385
|
-
return;
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
// TODO: Might wanna replace this or make it configurable. Look for other options.
|
|
389
|
-
const message = formatWithOptions(
|
|
390
|
-
{
|
|
391
|
-
colors: isTTY,
|
|
392
|
-
depth: 6,
|
|
393
|
-
},
|
|
394
|
-
...args,
|
|
395
|
-
);
|
|
396
|
-
|
|
397
|
-
yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
|
|
398
|
-
if (maxRetainedLogLines > 0) {
|
|
399
|
-
yield* Ref.update(logsRef, (logs) => {
|
|
400
|
-
const next = [...logs, message];
|
|
401
|
-
if (next.length <= maxRetainedLogLines) {
|
|
402
|
-
return next;
|
|
403
|
-
}
|
|
404
|
-
return next.slice(next.length - maxRetainedLogLines);
|
|
405
|
-
});
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
yield* markDirty;
|
|
409
|
-
});
|
|
410
|
-
|
|
411
|
-
const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
|
|
412
|
-
|
|
413
384
|
const getTask = (taskId: TaskId) =>
|
|
414
385
|
Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
|
|
415
386
|
|
|
@@ -419,7 +390,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
419
390
|
2,
|
|
420
391
|
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
421
392
|
Effect.gen(function* () {
|
|
422
|
-
const outerConsole = yield* Effect.console;
|
|
423
393
|
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
424
394
|
const resolvedParentId =
|
|
425
395
|
options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
|
|
@@ -431,10 +401,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
431
401
|
});
|
|
432
402
|
|
|
433
403
|
return yield* Effect.locally(
|
|
434
|
-
Effect.withConsole(
|
|
435
|
-
Effect.provideService(effect, Task, taskId),
|
|
436
|
-
makeProgressConsole(log, outerConsole),
|
|
437
|
-
),
|
|
404
|
+
Effect.withConsole(Effect.provideService(effect, Task, taskId), progressConsole),
|
|
438
405
|
currentParentRef,
|
|
439
406
|
Option.some(taskId),
|
|
440
407
|
);
|
|
@@ -486,29 +453,13 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
|
|
|
486
453
|
>() {
|
|
487
454
|
static readonly Default = Layer.unwrapEffect(
|
|
488
455
|
Effect.gen(function* () {
|
|
489
|
-
const themeOption = yield* Effect.serviceOption(Theme);
|
|
490
456
|
const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
|
|
491
|
-
const buildStageOption = yield* Effect.serviceOption(BuildStage);
|
|
492
|
-
const shrinkStageOption = yield* Effect.serviceOption(ShrinkStage);
|
|
493
|
-
const colorStageOption = yield* Effect.serviceOption(ColorStage);
|
|
494
457
|
const frameRendererOption = yield* Effect.serviceOption(FrameRenderer);
|
|
495
458
|
let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
|
|
496
459
|
|
|
497
460
|
if (Option.isNone(frameRendererOption)) {
|
|
498
461
|
layer = layer.pipe(Layer.provide(FrameRenderer.Default));
|
|
499
462
|
}
|
|
500
|
-
if (Option.isNone(colorStageOption)) {
|
|
501
|
-
layer = layer.pipe(Layer.provide(ColorStage.Default));
|
|
502
|
-
}
|
|
503
|
-
if (Option.isNone(shrinkStageOption)) {
|
|
504
|
-
layer = layer.pipe(Layer.provide(ShrinkStage.Default));
|
|
505
|
-
}
|
|
506
|
-
if (Option.isNone(buildStageOption)) {
|
|
507
|
-
layer = layer.pipe(Layer.provide(BuildStage.Default));
|
|
508
|
-
}
|
|
509
|
-
if (Option.isNone(themeOption)) {
|
|
510
|
-
layer = layer.pipe(Layer.provide(Theme.Default));
|
|
511
|
-
}
|
|
512
463
|
if (Option.isNone(terminalOption)) {
|
|
513
464
|
layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
|
|
514
465
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,17 +1,34 @@
|
|
|
1
1
|
import { Brand, Context, Effect, Option, Schema } from "effect";
|
|
2
2
|
import type { PartialDeep } from "type-fest";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ProgressColumn } from "./renderer";
|
|
4
4
|
|
|
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
|
-
|
|
11
|
-
|
|
9
|
+
width: Schema.Union(Schema.Number, Schema.Literal("fullwidth")),
|
|
10
|
+
columnGap: Schema.Number,
|
|
11
|
+
columns: Schema.Array(Schema.Unknown),
|
|
12
12
|
});
|
|
13
|
-
export type RendererConfigShape =
|
|
14
|
-
|
|
13
|
+
export type RendererConfigShape = {
|
|
14
|
+
readonly disableUserInput: boolean;
|
|
15
|
+
readonly renderIntervalMillis: number;
|
|
16
|
+
readonly nonTtyUpdateStep: number;
|
|
17
|
+
readonly width: number | "fullwidth";
|
|
18
|
+
readonly columnGap: number;
|
|
19
|
+
readonly columns: ReadonlyArray<ProgressColumn | string>;
|
|
20
|
+
};
|
|
21
|
+
const decodeRendererConfigSchemaSync = Schema.decodeUnknownSync(RendererConfigSchema);
|
|
22
|
+
export const decodeRendererConfigSync = (input: unknown): RendererConfigShape => {
|
|
23
|
+
if (typeof input === "object" && input !== null && "determinateTaskLayout" in input) {
|
|
24
|
+
throw new Error("determinateTaskLayout has been removed. Use columns instead.");
|
|
25
|
+
}
|
|
26
|
+
if (typeof input === "object" && input !== null && "maxTaskWidth" in input) {
|
|
27
|
+
throw new Error("maxTaskWidth has been removed. Use width on RendererConfig instead.");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return decodeRendererConfigSchemaSync(input) as RendererConfigShape;
|
|
31
|
+
};
|
|
15
32
|
|
|
16
33
|
export const ProgressBarConfigSchema = Schema.Struct({
|
|
17
34
|
spinnerFrames: Schema.NonEmptyArray(Schema.String),
|
|
@@ -27,9 +44,10 @@ export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarC
|
|
|
27
44
|
export const defaultRendererConfig: RendererConfigShape = {
|
|
28
45
|
disableUserInput: true,
|
|
29
46
|
renderIntervalMillis: 100, // 10 FPS
|
|
30
|
-
maxLogLines: 0,
|
|
31
47
|
nonTtyUpdateStep: 5,
|
|
32
|
-
|
|
48
|
+
width: 120,
|
|
49
|
+
columnGap: 1,
|
|
50
|
+
columns: [],
|
|
33
51
|
};
|
|
34
52
|
|
|
35
53
|
export const defaultProgressBarConfig: ProgressBarConfigShape = {
|
|
@@ -115,7 +133,6 @@ export interface RenderRow {
|
|
|
115
133
|
export interface TaskStore {
|
|
116
134
|
readonly tasks: Map<TaskId, TaskSnapshot>;
|
|
117
135
|
readonly renderOrder: ReadonlyArray<RenderRow>;
|
|
118
|
-
readonly themes: Map<TaskId, ThemeService>;
|
|
119
136
|
}
|
|
120
137
|
|
|
121
138
|
export interface ProgressService {
|
package/src/theme.ts
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
import chalk, { type ChalkInstance } from "chalk";
|
|
2
|
-
import { Context, Effect, Layer } from "effect";
|
|
3
|
-
|
|
4
|
-
export type ThemeRole =
|
|
5
|
-
| "plain"
|
|
6
|
-
| "barFill"
|
|
7
|
-
| "barEmpty"
|
|
8
|
-
| "barBracket"
|
|
9
|
-
| "spinner"
|
|
10
|
-
| "statusDone"
|
|
11
|
-
| "statusFailed"
|
|
12
|
-
| "text"
|
|
13
|
-
| "units"
|
|
14
|
-
| "eta"
|
|
15
|
-
| "elapsed"
|
|
16
|
-
| "treeConnector";
|
|
17
|
-
|
|
18
|
-
export type ThemeStyle = (text: string) => string;
|
|
19
|
-
|
|
20
|
-
export interface ThemeService {
|
|
21
|
-
readonly styles: Readonly<Record<ThemeRole, ThemeStyle>>;
|
|
22
|
-
readonly depthPalette?: (depth: number, role: ThemeRole) => ThemeStyle | undefined;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
export class Theme extends Context.Tag("stromseng.dev/effective-progress/Theme")<
|
|
26
|
-
Theme,
|
|
27
|
-
ThemeService
|
|
28
|
-
>() {
|
|
29
|
-
static readonly Default = Layer.succeed(
|
|
30
|
-
Theme,
|
|
31
|
-
Theme.of({
|
|
32
|
-
styles: {
|
|
33
|
-
plain: (text) => text,
|
|
34
|
-
barFill: chalk.blue,
|
|
35
|
-
barEmpty: chalk.white.dim,
|
|
36
|
-
barBracket: chalk.white.dim,
|
|
37
|
-
spinner: chalk.yellow,
|
|
38
|
-
statusDone: chalk.green,
|
|
39
|
-
statusFailed: chalk.red,
|
|
40
|
-
text: (text) => text,
|
|
41
|
-
units: chalk.whiteBright,
|
|
42
|
-
eta: chalk.gray,
|
|
43
|
-
elapsed: chalk.gray,
|
|
44
|
-
treeConnector: chalk.gray,
|
|
45
|
-
},
|
|
46
|
-
}),
|
|
47
|
-
);
|
|
48
|
-
|
|
49
|
-
static readonly Rainbow = Layer.unwrapEffect(
|
|
50
|
-
Effect.gen(function* () {
|
|
51
|
-
let colorNumber = 17;
|
|
52
|
-
yield* Effect.fork(
|
|
53
|
-
Effect.gen(function* () {
|
|
54
|
-
while (true) {
|
|
55
|
-
colorNumber = (colorNumber + 1) % 256;
|
|
56
|
-
yield* Effect.sleep("200 millis");
|
|
57
|
-
}
|
|
58
|
-
}),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
const dynamic =
|
|
62
|
-
(instance: ChalkInstance): ThemeStyle =>
|
|
63
|
-
(text) =>
|
|
64
|
-
instance.ansi256(colorNumber)(text);
|
|
65
|
-
|
|
66
|
-
return Layer.succeed(
|
|
67
|
-
Theme,
|
|
68
|
-
Theme.of({
|
|
69
|
-
styles: {
|
|
70
|
-
plain: (text) => text,
|
|
71
|
-
barFill: dynamic(chalk),
|
|
72
|
-
barEmpty: dynamic(chalk.dim),
|
|
73
|
-
barBracket: dynamic(chalk),
|
|
74
|
-
spinner: dynamic(chalk),
|
|
75
|
-
statusDone: dynamic(chalk.bold),
|
|
76
|
-
statusFailed: dynamic(chalk.bold),
|
|
77
|
-
text: dynamic(chalk),
|
|
78
|
-
units: dynamic(chalk),
|
|
79
|
-
eta: dynamic(chalk),
|
|
80
|
-
elapsed: dynamic(chalk),
|
|
81
|
-
treeConnector: dynamic(chalk),
|
|
82
|
-
},
|
|
83
|
-
}),
|
|
84
|
-
);
|
|
85
|
-
}),
|
|
86
|
-
);
|
|
87
|
-
}
|