effective-progress 0.3.0 → 0.4.0
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 +36 -50
- package/package.json +1 -1
- package/src/index.ts +2 -1
- package/src/renderer.ts +1157 -201
- package/src/runtime.ts +88 -29
- package/src/theme.ts +87 -0
- package/src/types.ts +7 -4
- package/src/colors.ts +0 -30
package/src/runtime.ts
CHANGED
|
@@ -3,10 +3,10 @@ import { dual } from "effect/Function";
|
|
|
3
3
|
import { mergeWith } from "es-toolkit/object";
|
|
4
4
|
import { formatWithOptions } from "node:util";
|
|
5
5
|
import type { PartialDeep } from "type-fest";
|
|
6
|
-
import { Colorizer, type ColorizerService } from "./colors";
|
|
7
6
|
import { makeProgressConsole } from "./console";
|
|
8
|
-
import {
|
|
7
|
+
import { BuildStage, ColorStage, FrameRenderer, ShrinkStage } from "./renderer";
|
|
9
8
|
import { ProgressTerminal } from "./terminal";
|
|
9
|
+
import { Theme, type ThemeService } from "./theme";
|
|
10
10
|
import type {
|
|
11
11
|
AddTaskOptions,
|
|
12
12
|
ProgressService,
|
|
@@ -88,6 +88,19 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
88
88
|
});
|
|
89
89
|
};
|
|
90
90
|
|
|
91
|
+
const withTransient = (snapshot: TaskSnapshot, transient: boolean): TaskSnapshot =>
|
|
92
|
+
new TaskSnapshot({
|
|
93
|
+
id: snapshot.id,
|
|
94
|
+
parentId: snapshot.parentId,
|
|
95
|
+
description: snapshot.description,
|
|
96
|
+
status: snapshot.status,
|
|
97
|
+
transient,
|
|
98
|
+
units: snapshot.units,
|
|
99
|
+
config: snapshot.config,
|
|
100
|
+
startedAt: snapshot.startedAt,
|
|
101
|
+
completedAt: snapshot.completedAt,
|
|
102
|
+
});
|
|
103
|
+
|
|
91
104
|
const findInsertionIndex = (
|
|
92
105
|
renderOrder: ReadonlyArray<RenderRow>,
|
|
93
106
|
parentId: TaskId | null,
|
|
@@ -133,6 +146,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
133
146
|
),
|
|
134
147
|
);
|
|
135
148
|
const terminal = yield* ProgressTerminal;
|
|
149
|
+
const frameRenderer = yield* FrameRenderer;
|
|
136
150
|
const isTTY = yield* terminal.isTTY;
|
|
137
151
|
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
138
152
|
|
|
@@ -140,7 +154,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
140
154
|
const storeRef = yield* Ref.make<TaskStore>({
|
|
141
155
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
142
156
|
renderOrder: [],
|
|
143
|
-
|
|
157
|
+
themes: new Map<TaskId, ThemeService>(),
|
|
144
158
|
});
|
|
145
159
|
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
146
160
|
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
@@ -149,7 +163,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
149
163
|
const scope = yield* Effect.scope;
|
|
150
164
|
|
|
151
165
|
yield* Effect.forkIn(
|
|
152
|
-
|
|
166
|
+
frameRenderer.run({
|
|
153
167
|
storeRef,
|
|
154
168
|
logsRef,
|
|
155
169
|
pendingLogsRef,
|
|
@@ -158,7 +172,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
158
172
|
isTTY,
|
|
159
173
|
rendererConfig,
|
|
160
174
|
maxRetainedLogLines,
|
|
161
|
-
),
|
|
175
|
+
}),
|
|
162
176
|
scope,
|
|
163
177
|
);
|
|
164
178
|
|
|
@@ -170,7 +184,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
170
184
|
options.parentId === undefined
|
|
171
185
|
? yield* FiberRef.get(currentParentRef)
|
|
172
186
|
: Option.some(options.parentId);
|
|
173
|
-
const
|
|
187
|
+
const themeOption = yield* Effect.serviceOption(Theme);
|
|
174
188
|
const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
|
|
175
189
|
const units =
|
|
176
190
|
options.total === undefined || options.total <= 0
|
|
@@ -192,7 +206,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
192
206
|
parentId: parentIdValue,
|
|
193
207
|
description: options.description,
|
|
194
208
|
status: "running",
|
|
195
|
-
transient: options.transient ?? false,
|
|
209
|
+
transient: parentSnapshot?.transient ?? options.transient ?? false,
|
|
196
210
|
units,
|
|
197
211
|
config: resolvedProgressBarConfig,
|
|
198
212
|
startedAt: now,
|
|
@@ -205,11 +219,11 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
205
219
|
const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
|
|
206
220
|
const nextOrder = [...s.renderOrder];
|
|
207
221
|
nextOrder.splice(index, 0, { id: taskId, depth });
|
|
208
|
-
const
|
|
209
|
-
if (Option.isSome(
|
|
210
|
-
|
|
222
|
+
const nextThemes = new Map(s.themes);
|
|
223
|
+
if (Option.isSome(themeOption)) {
|
|
224
|
+
nextThemes.set(taskId, themeOption.value);
|
|
211
225
|
}
|
|
212
|
-
return { tasks: nextTasks, renderOrder: nextOrder,
|
|
226
|
+
return { tasks: nextTasks, renderOrder: nextOrder, themes: nextThemes };
|
|
213
227
|
});
|
|
214
228
|
yield* markDirty;
|
|
215
229
|
|
|
@@ -221,8 +235,32 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
221
235
|
const snapshot = store.tasks.get(taskId);
|
|
222
236
|
if (!snapshot) return store;
|
|
223
237
|
const nextTasks = new Map(store.tasks);
|
|
224
|
-
|
|
225
|
-
|
|
238
|
+
const nextSnapshot = updatedSnapshot(snapshot, options);
|
|
239
|
+
nextTasks.set(taskId, nextSnapshot);
|
|
240
|
+
|
|
241
|
+
if (options.transient !== undefined) {
|
|
242
|
+
for (const [candidateId, candidate] of store.tasks.entries()) {
|
|
243
|
+
if (candidateId === taskId) {
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
let parentId = candidate.parentId;
|
|
248
|
+
let isDescendant = false;
|
|
249
|
+
while (parentId !== null) {
|
|
250
|
+
if (parentId === taskId) {
|
|
251
|
+
isDescendant = true;
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
parentId = store.tasks.get(parentId)?.parentId ?? null;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (isDescendant) {
|
|
258
|
+
nextTasks.set(candidateId, withTransient(candidate, nextSnapshot.transient));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
|
|
226
264
|
}).pipe(Effect.zipRight(markDirty));
|
|
227
265
|
|
|
228
266
|
const advanceTask = (taskId: TaskId, amount = 1) =>
|
|
@@ -256,7 +294,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
256
294
|
}),
|
|
257
295
|
);
|
|
258
296
|
|
|
259
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder,
|
|
297
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
|
|
260
298
|
}).pipe(Effect.zipRight(markDirty));
|
|
261
299
|
|
|
262
300
|
const completeTask = (taskId: TaskId) =>
|
|
@@ -269,12 +307,12 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
269
307
|
const nextTasks = new Map(store.tasks);
|
|
270
308
|
if (snapshot.transient) {
|
|
271
309
|
nextTasks.delete(taskId);
|
|
272
|
-
const
|
|
273
|
-
|
|
310
|
+
const nextThemes = new Map(store.themes);
|
|
311
|
+
nextThemes.delete(taskId);
|
|
274
312
|
return {
|
|
275
313
|
tasks: nextTasks,
|
|
276
314
|
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
277
|
-
|
|
315
|
+
themes: nextThemes,
|
|
278
316
|
};
|
|
279
317
|
}
|
|
280
318
|
|
|
@@ -298,7 +336,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
298
336
|
completedAt: now,
|
|
299
337
|
}),
|
|
300
338
|
);
|
|
301
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder,
|
|
339
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
|
|
302
340
|
});
|
|
303
341
|
yield* markDirty;
|
|
304
342
|
});
|
|
@@ -313,12 +351,12 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
313
351
|
const nextTasks = new Map(store.tasks);
|
|
314
352
|
if (snapshot.transient) {
|
|
315
353
|
nextTasks.delete(taskId);
|
|
316
|
-
const
|
|
317
|
-
|
|
354
|
+
const nextThemes = new Map(store.themes);
|
|
355
|
+
nextThemes.delete(taskId);
|
|
318
356
|
return {
|
|
319
357
|
tasks: nextTasks,
|
|
320
358
|
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
321
|
-
|
|
359
|
+
themes: nextThemes,
|
|
322
360
|
};
|
|
323
361
|
}
|
|
324
362
|
|
|
@@ -336,7 +374,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
336
374
|
completedAt: now,
|
|
337
375
|
}),
|
|
338
376
|
);
|
|
339
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder,
|
|
377
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
|
|
340
378
|
});
|
|
341
379
|
yield* markDirty;
|
|
342
380
|
});
|
|
@@ -389,7 +427,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
389
427
|
const taskId = yield* addTask({
|
|
390
428
|
...options,
|
|
391
429
|
parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : undefined,
|
|
392
|
-
transient: options.transient
|
|
430
|
+
transient: options.transient,
|
|
393
431
|
});
|
|
394
432
|
|
|
395
433
|
return yield* Effect.locally(
|
|
@@ -448,13 +486,34 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
|
|
|
448
486
|
>() {
|
|
449
487
|
static readonly Default = Layer.unwrapEffect(
|
|
450
488
|
Effect.gen(function* () {
|
|
451
|
-
const
|
|
489
|
+
const themeOption = yield* Effect.serviceOption(Theme);
|
|
452
490
|
const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
|
|
453
|
-
const
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
);
|
|
491
|
+
const buildStageOption = yield* Effect.serviceOption(BuildStage);
|
|
492
|
+
const shrinkStageOption = yield* Effect.serviceOption(ShrinkStage);
|
|
493
|
+
const colorStageOption = yield* Effect.serviceOption(ColorStage);
|
|
494
|
+
const frameRendererOption = yield* Effect.serviceOption(FrameRenderer);
|
|
495
|
+
let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
|
|
496
|
+
|
|
497
|
+
if (Option.isNone(frameRendererOption)) {
|
|
498
|
+
layer = layer.pipe(Layer.provide(FrameRenderer.Default));
|
|
499
|
+
}
|
|
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
|
+
if (Option.isNone(terminalOption)) {
|
|
513
|
+
layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
return layer as Layer.Layer<Progress, never, never>;
|
|
458
517
|
}),
|
|
459
518
|
);
|
|
460
519
|
}
|
package/src/theme.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { Brand, Context, Effect, Option, Schema } from "effect";
|
|
2
2
|
import type { PartialDeep } from "type-fest";
|
|
3
|
-
import type {
|
|
3
|
+
import type { ThemeService } from "./theme";
|
|
4
4
|
|
|
5
5
|
export const RendererConfigSchema = Schema.Struct({
|
|
6
6
|
disableUserInput: Schema.Boolean,
|
|
7
7
|
renderIntervalMillis: Schema.Number,
|
|
8
8
|
maxLogLines: Schema.optional(Schema.Number),
|
|
9
9
|
nonTtyUpdateStep: Schema.Number,
|
|
10
|
+
determinateTaskLayout: Schema.Literal("single-line", "two-lines"),
|
|
11
|
+
maxTaskWidth: Schema.optional(Schema.Number),
|
|
10
12
|
});
|
|
11
13
|
export type RendererConfigShape = typeof RendererConfigSchema.Type;
|
|
12
14
|
export const decodeRendererConfigSync = Schema.decodeUnknownSync(RendererConfigSchema);
|
|
@@ -27,11 +29,12 @@ export const defaultRendererConfig: RendererConfigShape = {
|
|
|
27
29
|
renderIntervalMillis: 100, // 10 FPS
|
|
28
30
|
maxLogLines: 0,
|
|
29
31
|
nonTtyUpdateStep: 5,
|
|
32
|
+
determinateTaskLayout: "single-line",
|
|
30
33
|
};
|
|
31
34
|
|
|
32
35
|
export const defaultProgressBarConfig: ProgressBarConfigShape = {
|
|
33
|
-
spinnerFrames: ["
|
|
34
|
-
barWidth:
|
|
36
|
+
spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
37
|
+
barWidth: 40,
|
|
35
38
|
fillChar: "━",
|
|
36
39
|
emptyChar: "─",
|
|
37
40
|
leftBracket: "",
|
|
@@ -112,7 +115,7 @@ export interface RenderRow {
|
|
|
112
115
|
export interface TaskStore {
|
|
113
116
|
readonly tasks: Map<TaskId, TaskSnapshot>;
|
|
114
117
|
readonly renderOrder: ReadonlyArray<RenderRow>;
|
|
115
|
-
readonly
|
|
118
|
+
readonly themes: Map<TaskId, ThemeService>;
|
|
116
119
|
}
|
|
117
120
|
|
|
118
121
|
export interface ProgressService {
|
package/src/colors.ts
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import { Context, Layer } from "effect";
|
|
3
|
-
|
|
4
|
-
export interface ColorizerService {
|
|
5
|
-
readonly fill: (text: string) => string;
|
|
6
|
-
readonly empty: (text: string) => string;
|
|
7
|
-
readonly brackets: (text: string) => string;
|
|
8
|
-
readonly percent: (text: string) => string;
|
|
9
|
-
readonly spinner: (text: string) => string;
|
|
10
|
-
readonly done: (text: string) => string;
|
|
11
|
-
readonly failed: (text: string) => string;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export class Colorizer extends Context.Tag("stromseng.dev/effective-progress/Colorizer")<
|
|
15
|
-
Colorizer,
|
|
16
|
-
ColorizerService
|
|
17
|
-
>() {
|
|
18
|
-
static readonly Default = Layer.succeed(
|
|
19
|
-
Colorizer,
|
|
20
|
-
Colorizer.of({
|
|
21
|
-
fill: chalk.blue,
|
|
22
|
-
empty: chalk.white.dim,
|
|
23
|
-
brackets: chalk.white.dim,
|
|
24
|
-
percent: chalk.white.bold,
|
|
25
|
-
spinner: chalk.yellow,
|
|
26
|
-
done: chalk.green,
|
|
27
|
-
failed: chalk.red,
|
|
28
|
-
}),
|
|
29
|
-
);
|
|
30
|
-
}
|