effective-progress 0.2.3 → 0.3.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 +51 -32
- package/package.json +1 -1
- package/src/api.ts +20 -7
- package/src/colors.ts +19 -145
- package/src/renderer.ts +182 -185
- package/src/runtime.ts +111 -77
- package/src/terminal.ts +12 -10
- package/src/types.ts +10 -10
package/README.md
CHANGED
|
@@ -105,10 +105,6 @@ const configured = program.pipe(
|
|
|
105
105
|
}),
|
|
106
106
|
Effect.provideService(Progress.ProgressBarConfig, {
|
|
107
107
|
barWidth: 36,
|
|
108
|
-
colors: {
|
|
109
|
-
fill: { kind: "hex", value: "#00b894" },
|
|
110
|
-
spinner: { kind: "ansi256", value: 214 },
|
|
111
|
-
},
|
|
112
108
|
}),
|
|
113
109
|
);
|
|
114
110
|
|
|
@@ -124,9 +120,6 @@ yield *
|
|
|
124
120
|
progressbar: {
|
|
125
121
|
barWidth: 20,
|
|
126
122
|
spinnerFrames: [".", "o", "O", "0"],
|
|
127
|
-
colors: {
|
|
128
|
-
spinner: { kind: "named", value: "magentaBright" },
|
|
129
|
-
},
|
|
130
123
|
},
|
|
131
124
|
});
|
|
132
125
|
```
|
|
@@ -177,38 +170,64 @@ const program = Progress.task(
|
|
|
177
170
|
|
|
178
171
|
## Progress bar colors
|
|
179
172
|
|
|
180
|
-
`
|
|
173
|
+
Colors are configured through the `Colorizer` service. You can set a global colorizer, or provide per-task overrides using `Effect.provideService`.
|
|
174
|
+
|
|
175
|
+
### Global colorizer
|
|
176
|
+
|
|
177
|
+
Provide a custom `Colorizer` at the top level to change colors for all tasks:
|
|
181
178
|
|
|
182
179
|
```ts
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
180
|
+
import chalk from "chalk";
|
|
181
|
+
import { Effect } from "effect";
|
|
182
|
+
import * as Progress from "effective-progress";
|
|
183
|
+
|
|
184
|
+
const program = Progress.task(myEffect, { description: "Work" }).pipe(
|
|
185
|
+
Effect.provideService(
|
|
186
|
+
Progress.Colorizer,
|
|
187
|
+
Progress.Colorizer.of({
|
|
188
|
+
fill: chalk.hex("#00b894"),
|
|
189
|
+
empty: chalk.white.dim,
|
|
190
|
+
brackets: chalk.rgb(180, 190, 210),
|
|
191
|
+
percent: chalk.whiteBright.bold,
|
|
192
|
+
spinner: chalk.ansi256(214),
|
|
193
|
+
done: chalk.greenBright,
|
|
194
|
+
failed: chalk.redBright.bold,
|
|
195
|
+
}),
|
|
196
|
+
),
|
|
197
|
+
);
|
|
200
198
|
```
|
|
201
199
|
|
|
202
|
-
|
|
200
|
+
### Per-task colorizer
|
|
201
|
+
|
|
202
|
+
Wrap any effect with `Effect.provideService(Colorizer, ...)` to override colors for that task (and its children). The colorizer is captured at task-creation time, so each task can have its own colors:
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
```ts
|
|
205
|
+
Progress.forEach(
|
|
206
|
+
["fetch", "transform", "persist"],
|
|
207
|
+
(stage) => Effect.gen(function* () {
|
|
208
|
+
yield* Effect.sleep("500 millis");
|
|
209
|
+
return stage;
|
|
210
|
+
}),
|
|
211
|
+
{ description: "Worker pipeline" },
|
|
212
|
+
).pipe(
|
|
213
|
+
Effect.provideService(
|
|
214
|
+
Progress.Colorizer,
|
|
215
|
+
Progress.Colorizer.of({
|
|
216
|
+
fill: chalk.red,
|
|
217
|
+
empty: chalk.white.dim,
|
|
218
|
+
brackets: chalk.white.dim,
|
|
219
|
+
percent: chalk.white.bold,
|
|
220
|
+
spinner: chalk.magentaBright,
|
|
221
|
+
done: chalk.greenBright,
|
|
222
|
+
failed: chalk.redBright,
|
|
223
|
+
}),
|
|
224
|
+
),
|
|
225
|
+
);
|
|
226
|
+
```
|
|
208
227
|
|
|
209
|
-
|
|
228
|
+
Tasks inherit the colorizer from their parent task.
|
|
210
229
|
|
|
211
|
-
|
|
230
|
+
Each `ColorizerService` slot (`fill`, `empty`, `brackets`, `percent`, `spinner`, `done`, `failed`) is a `(text: string) => string` function. Use any chalk style — named colors, hex, rgb, ansi256, and modifiers like `.bold` or `.dim` all work.
|
|
212
231
|
|
|
213
232
|
## Dependencies & package size
|
|
214
233
|
|
package/package.json
CHANGED
package/src/api.ts
CHANGED
|
@@ -1,11 +1,20 @@
|
|
|
1
|
-
import { Effect, Exit } from "effect";
|
|
1
|
+
import { Effect, Exit, Option } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
3
|
import type { Concurrency } from "effect/Types";
|
|
4
|
-
import { Progress
|
|
4
|
+
import { Progress } from "./runtime";
|
|
5
5
|
import { Task } from "./types";
|
|
6
6
|
import type { AddTaskOptions, TrackOptions } from "./types";
|
|
7
7
|
import { inferTotal } from "./utils";
|
|
8
8
|
|
|
9
|
+
const provideProgress = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
10
|
+
Effect.gen(function* () {
|
|
11
|
+
const existing = yield* Effect.serviceOption(Progress);
|
|
12
|
+
if (Option.isSome(existing)) {
|
|
13
|
+
return yield* Effect.provideService(effect, Progress, existing.value);
|
|
14
|
+
}
|
|
15
|
+
return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
|
|
16
|
+
});
|
|
17
|
+
|
|
9
18
|
export interface EffectExecutionOptions {
|
|
10
19
|
readonly concurrency?: Concurrency;
|
|
11
20
|
readonly batching?: boolean | "inherit";
|
|
@@ -19,7 +28,9 @@ export interface EffectAllExecutionOptions extends EffectExecutionOptions {
|
|
|
19
28
|
|
|
20
29
|
export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
|
|
21
30
|
export type AllReturn<
|
|
22
|
-
Arg extends
|
|
31
|
+
Arg extends
|
|
32
|
+
| ReadonlyArray<Effect.Effect<any, any, any>>
|
|
33
|
+
| Record<string, Effect.Effect<any, any, any>>,
|
|
23
34
|
O extends EffectAllExecutionOptions,
|
|
24
35
|
> = [
|
|
25
36
|
[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>]
|
|
@@ -48,7 +59,7 @@ export const task: {
|
|
|
48
59
|
} = dual(
|
|
49
60
|
2,
|
|
50
61
|
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
51
|
-
|
|
62
|
+
provideProgress(
|
|
52
63
|
Effect.gen(function* () {
|
|
53
64
|
const progress = yield* Progress;
|
|
54
65
|
return yield* progress.withTask(effect, options);
|
|
@@ -56,7 +67,9 @@ export const task: {
|
|
|
56
67
|
) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
|
|
57
68
|
);
|
|
58
69
|
|
|
59
|
-
type AllArg =
|
|
70
|
+
type AllArg =
|
|
71
|
+
| ReadonlyArray<Effect.Effect<any, any, any>>
|
|
72
|
+
| Record<string, Effect.Effect<any, any, any>>;
|
|
60
73
|
|
|
61
74
|
const wrapEffects = (
|
|
62
75
|
effects: AllArg,
|
|
@@ -83,7 +96,7 @@ export const all: {
|
|
|
83
96
|
effects: Arg,
|
|
84
97
|
options: Omit<TrackOptions, "total"> & O,
|
|
85
98
|
) =>
|
|
86
|
-
|
|
99
|
+
provideProgress(
|
|
87
100
|
Effect.gen(function* () {
|
|
88
101
|
const progress = yield* Progress;
|
|
89
102
|
return yield* progress.runTask(
|
|
@@ -143,7 +156,7 @@ export const forEach: {
|
|
|
143
156
|
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
144
157
|
options: ForEachOptions,
|
|
145
158
|
) =>
|
|
146
|
-
|
|
159
|
+
provideProgress(
|
|
147
160
|
Effect.gen(function* () {
|
|
148
161
|
const progress = yield* Progress;
|
|
149
162
|
|
package/src/colors.ts
CHANGED
|
@@ -1,141 +1,7 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
|
-
import
|
|
3
|
-
import { Schema } from "effect";
|
|
2
|
+
import { Context, Layer } from "effect";
|
|
4
3
|
|
|
5
|
-
export
|
|
6
|
-
"bold",
|
|
7
|
-
"dim",
|
|
8
|
-
"italic",
|
|
9
|
-
"underline",
|
|
10
|
-
"inverse",
|
|
11
|
-
"hidden",
|
|
12
|
-
"strikethrough",
|
|
13
|
-
);
|
|
14
|
-
export type StyleModifier = typeof StyleModifierSchema.Type;
|
|
15
|
-
|
|
16
|
-
export const NamedColorSchema = Schema.Literal(
|
|
17
|
-
"black",
|
|
18
|
-
"red",
|
|
19
|
-
"green",
|
|
20
|
-
"yellow",
|
|
21
|
-
"blue",
|
|
22
|
-
"magenta",
|
|
23
|
-
"cyan",
|
|
24
|
-
"white",
|
|
25
|
-
"blackBright",
|
|
26
|
-
"redBright",
|
|
27
|
-
"greenBright",
|
|
28
|
-
"yellowBright",
|
|
29
|
-
"blueBright",
|
|
30
|
-
"magentaBright",
|
|
31
|
-
"cyanBright",
|
|
32
|
-
"whiteBright",
|
|
33
|
-
);
|
|
34
|
-
export type NamedColor = typeof NamedColorSchema.Type;
|
|
35
|
-
|
|
36
|
-
const HexColorSchema = Schema.String.pipe(Schema.pattern(/^#(?:[A-Fa-f0-9]{3}|[A-Fa-f0-9]{6})$/));
|
|
37
|
-
const ColorChannelSchema = Schema.Number.pipe(
|
|
38
|
-
Schema.int(),
|
|
39
|
-
Schema.greaterThanOrEqualTo(0),
|
|
40
|
-
Schema.lessThanOrEqualTo(255),
|
|
41
|
-
);
|
|
42
|
-
const Ansi256Schema = Schema.Number.pipe(
|
|
43
|
-
Schema.int(),
|
|
44
|
-
Schema.greaterThanOrEqualTo(0),
|
|
45
|
-
Schema.lessThanOrEqualTo(255),
|
|
46
|
-
);
|
|
47
|
-
const ModifiersSchema = Schema.optional(Schema.Array(StyleModifierSchema));
|
|
48
|
-
|
|
49
|
-
export const NamedColorStyleSchema = Schema.Struct({
|
|
50
|
-
kind: Schema.Literal("named"),
|
|
51
|
-
value: NamedColorSchema,
|
|
52
|
-
modifiers: ModifiersSchema,
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
export const HexColorStyleSchema = Schema.Struct({
|
|
56
|
-
kind: Schema.Literal("hex"),
|
|
57
|
-
value: HexColorSchema,
|
|
58
|
-
modifiers: ModifiersSchema,
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
export const RgbColorStyleSchema = Schema.Struct({
|
|
62
|
-
kind: Schema.Literal("rgb"),
|
|
63
|
-
value: Schema.Struct({
|
|
64
|
-
r: ColorChannelSchema,
|
|
65
|
-
g: ColorChannelSchema,
|
|
66
|
-
b: ColorChannelSchema,
|
|
67
|
-
}),
|
|
68
|
-
modifiers: ModifiersSchema,
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
export const Ansi256ColorStyleSchema = Schema.Struct({
|
|
72
|
-
kind: Schema.Literal("ansi256"),
|
|
73
|
-
value: Ansi256Schema,
|
|
74
|
-
modifiers: ModifiersSchema,
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
export const ColorStyleSchema = Schema.Union(
|
|
78
|
-
NamedColorStyleSchema,
|
|
79
|
-
HexColorStyleSchema,
|
|
80
|
-
RgbColorStyleSchema,
|
|
81
|
-
Ansi256ColorStyleSchema,
|
|
82
|
-
);
|
|
83
|
-
export type ColorStyle = typeof ColorStyleSchema.Type;
|
|
84
|
-
|
|
85
|
-
export const ProgressBarColorsSchema = Schema.Struct({
|
|
86
|
-
fill: ColorStyleSchema,
|
|
87
|
-
empty: ColorStyleSchema,
|
|
88
|
-
brackets: ColorStyleSchema,
|
|
89
|
-
percent: ColorStyleSchema,
|
|
90
|
-
spinner: ColorStyleSchema,
|
|
91
|
-
done: ColorStyleSchema,
|
|
92
|
-
failed: ColorStyleSchema,
|
|
93
|
-
});
|
|
94
|
-
export type ProgressBarColors = typeof ProgressBarColorsSchema.Type;
|
|
95
|
-
|
|
96
|
-
export const defaultProgressBarColors: ProgressBarColors = {
|
|
97
|
-
fill: { kind: "named", value: "cyan" },
|
|
98
|
-
empty: { kind: "named", value: "white", modifiers: ["dim"] },
|
|
99
|
-
brackets: { kind: "named", value: "white", modifiers: ["dim"] },
|
|
100
|
-
percent: { kind: "named", value: "white", modifiers: ["bold"] },
|
|
101
|
-
spinner: { kind: "named", value: "yellow" },
|
|
102
|
-
done: { kind: "named", value: "green" },
|
|
103
|
-
failed: { kind: "named", value: "red" },
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const applyModifier = (instance: ChalkInstance, modifier: StyleModifier): ChalkInstance =>
|
|
107
|
-
instance[modifier];
|
|
108
|
-
|
|
109
|
-
const applyNamedColor = (instance: ChalkInstance, color: NamedColor): ChalkInstance =>
|
|
110
|
-
instance[color];
|
|
111
|
-
|
|
112
|
-
const resolveBaseStyle = (style: ColorStyle): ChalkInstance => {
|
|
113
|
-
switch (style.kind) {
|
|
114
|
-
case "named":
|
|
115
|
-
return applyNamedColor(chalk, style.value);
|
|
116
|
-
case "hex":
|
|
117
|
-
return chalk.hex(style.value);
|
|
118
|
-
case "rgb":
|
|
119
|
-
return chalk.rgb(style.value.r, style.value.g, style.value.b);
|
|
120
|
-
case "ansi256":
|
|
121
|
-
return chalk.ansi256(style.value);
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
const applyModifiers = (instance: ChalkInstance, modifiers: ReadonlyArray<StyleModifier>) => {
|
|
126
|
-
let styled = instance;
|
|
127
|
-
for (const modifier of modifiers) {
|
|
128
|
-
styled = applyModifier(styled, modifier);
|
|
129
|
-
}
|
|
130
|
-
return styled;
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
export const applyColorStyle = (style: ColorStyle, text: string): string => {
|
|
134
|
-
const styled = applyModifiers(resolveBaseStyle(style), style.modifiers ?? []);
|
|
135
|
-
return styled(text);
|
|
136
|
-
};
|
|
137
|
-
|
|
138
|
-
export interface CompiledProgressBarColors {
|
|
4
|
+
export interface ColorizerService {
|
|
139
5
|
readonly fill: (text: string) => string;
|
|
140
6
|
readonly empty: (text: string) => string;
|
|
141
7
|
readonly brackets: (text: string) => string;
|
|
@@ -145,12 +11,20 @@ export interface CompiledProgressBarColors {
|
|
|
145
11
|
readonly failed: (text: string) => string;
|
|
146
12
|
}
|
|
147
13
|
|
|
148
|
-
export
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
+
}
|
package/src/renderer.ts
CHANGED
|
@@ -1,9 +1,5 @@
|
|
|
1
|
-
import { Effect, Ref
|
|
2
|
-
import {
|
|
3
|
-
type CompiledProgressBarColors,
|
|
4
|
-
compileProgressBarColors,
|
|
5
|
-
ProgressBarColorsSchema,
|
|
6
|
-
} from "./colors";
|
|
1
|
+
import { Clock, Duration, Effect, Ref } from "effect";
|
|
2
|
+
import { Colorizer, type ColorizerService } from "./colors";
|
|
7
3
|
import type { ProgressTerminalService } from "./terminal";
|
|
8
4
|
import type { ProgressBarConfigShape, RendererConfigShape, TaskStore } from "./types";
|
|
9
5
|
import { DeterminateTaskUnits, TaskSnapshot } from "./types";
|
|
@@ -12,12 +8,11 @@ const HIDE_CURSOR = "\x1b[?25l";
|
|
|
12
8
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
13
9
|
const CLEAR_LINE = "\x1b[2K";
|
|
14
10
|
const MOVE_UP_ONE = "\x1b[1A";
|
|
15
|
-
const encodeProgressBarColorsKey = Schema.encodeSync(Schema.parseJson(ProgressBarColorsSchema));
|
|
16
11
|
|
|
17
12
|
const renderDeterminate = (
|
|
18
13
|
units: DeterminateTaskUnits,
|
|
19
14
|
progressbar: ProgressBarConfigShape,
|
|
20
|
-
colors:
|
|
15
|
+
colors: ColorizerService,
|
|
21
16
|
): string => {
|
|
22
17
|
const safeTotal = units.total <= 0 ? 1 : units.total;
|
|
23
18
|
const ratio = Math.min(1, Math.max(0, units.completed / safeTotal));
|
|
@@ -27,34 +22,45 @@ const renderDeterminate = (
|
|
|
27
22
|
return `${colors.brackets(progressbar.leftBracket)}${bar}${colors.brackets(progressbar.rightBracket)} ${units.completed}/${units.total} ${colors.percent(percent + "%")}`;
|
|
28
23
|
};
|
|
29
24
|
|
|
25
|
+
const formatElapsed = (snapshot: TaskSnapshot, now: number): string => {
|
|
26
|
+
const elapsedMillis = (snapshot.completedAt ?? now) - snapshot.startedAt;
|
|
27
|
+
const duration =
|
|
28
|
+
snapshot.status === "running"
|
|
29
|
+
? Duration.seconds(Math.floor(elapsedMillis / 1000))
|
|
30
|
+
: Duration.millis(elapsedMillis);
|
|
31
|
+
return ` (${Duration.format(duration)})`;
|
|
32
|
+
};
|
|
33
|
+
|
|
30
34
|
const buildTaskLine = (
|
|
31
35
|
snapshot: TaskSnapshot,
|
|
32
36
|
depth: number,
|
|
33
37
|
tick: number,
|
|
34
|
-
colors:
|
|
38
|
+
colors: ColorizerService,
|
|
39
|
+
now: number,
|
|
35
40
|
): string => {
|
|
36
41
|
const progressbar = snapshot.config;
|
|
37
42
|
const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
|
|
43
|
+
const elapsed = formatElapsed(snapshot, now);
|
|
38
44
|
|
|
39
45
|
if (snapshot.status === "failed") {
|
|
40
|
-
return `${prefix}${colors.failed("[failed]")}`;
|
|
46
|
+
return `${prefix}${colors.failed("[failed]")}${elapsed}`;
|
|
41
47
|
}
|
|
42
48
|
|
|
43
49
|
if (snapshot.status === "done") {
|
|
44
50
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
45
|
-
return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}`;
|
|
51
|
+
return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}${elapsed}`;
|
|
46
52
|
}
|
|
47
|
-
return `${prefix}${colors.done("[done]")}`;
|
|
53
|
+
return `${prefix}${colors.done("[done]")}${elapsed}`;
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
51
|
-
return prefix + renderDeterminate(snapshot.units, progressbar, colors);
|
|
57
|
+
return prefix + renderDeterminate(snapshot.units, progressbar, colors) + elapsed;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
const frames = progressbar.spinnerFrames;
|
|
55
61
|
const frameIndex = (snapshot.units.spinnerFrame + tick) % frames.length;
|
|
56
62
|
const frame = frames[frameIndex] ?? frames[0]!;
|
|
57
|
-
return `${prefix}${colors.spinner(frame)}`;
|
|
63
|
+
return `${prefix}${colors.spinner(frame)}${elapsed}`;
|
|
58
64
|
};
|
|
59
65
|
|
|
60
66
|
export const runProgressServiceRenderer = (
|
|
@@ -66,209 +72,200 @@ export const runProgressServiceRenderer = (
|
|
|
66
72
|
isTTY: boolean,
|
|
67
73
|
rendererConfig: RendererConfigShape,
|
|
68
74
|
maxRetainedLogLines: number,
|
|
69
|
-
) =>
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
75
|
+
) =>
|
|
76
|
+
Effect.gen(function* () {
|
|
77
|
+
const colorizer = yield* Colorizer;
|
|
78
|
+
const retainLogHistory = maxRetainedLogLines > 0;
|
|
79
|
+
let previousLineCount = 0;
|
|
80
|
+
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
81
|
+
let tick = 0;
|
|
82
|
+
let rendererActive = false;
|
|
83
|
+
let sessionActive = false;
|
|
84
|
+
|
|
85
|
+
const clipTTYFrameLines = (lines: ReadonlyArray<string>) =>
|
|
86
|
+
Effect.gen(function* () {
|
|
87
|
+
const terminalRows = yield* terminal.stderrRows;
|
|
88
|
+
if (terminalRows === undefined) {
|
|
89
|
+
return lines;
|
|
90
|
+
}
|
|
84
91
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
92
|
+
const visibleLineLimit = Math.max(1, terminalRows - 1);
|
|
93
|
+
if (lines.length <= visibleLineLimit) {
|
|
94
|
+
return lines;
|
|
95
|
+
}
|
|
89
96
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (terminalRows === undefined) {
|
|
94
|
-
return lines;
|
|
95
|
-
}
|
|
97
|
+
if (visibleLineLimit === 1) {
|
|
98
|
+
return [`... ${lines.length} lines hidden`];
|
|
99
|
+
}
|
|
96
100
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
+
const hiddenLineCount = lines.length - visibleLineLimit + 1;
|
|
102
|
+
return [
|
|
103
|
+
`... ${hiddenLineCount} lines hidden (showing latest lines)`,
|
|
104
|
+
...lines.slice(lines.length - (visibleLineLimit - 1)),
|
|
105
|
+
];
|
|
106
|
+
});
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
108
|
+
const startTTYSession = Effect.gen(function* () {
|
|
109
|
+
if (!isTTY || sessionActive) {
|
|
110
|
+
return;
|
|
104
111
|
}
|
|
105
112
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
`... ${hiddenLineCount} lines hidden (showing latest lines)`,
|
|
109
|
-
...lines.slice(lines.length - (visibleLineLimit - 1)),
|
|
110
|
-
];
|
|
113
|
+
yield* terminal.writeStderr(HIDE_CURSOR);
|
|
114
|
+
sessionActive = true;
|
|
111
115
|
});
|
|
112
116
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
yield* terminal.writeStderr(HIDE_CURSOR);
|
|
119
|
-
sessionActive = true;
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
const stopTTYSession = Effect.gen(function* () {
|
|
123
|
-
if (!isTTY || !sessionActive) {
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
117
|
+
const stopTTYSession = Effect.gen(function* () {
|
|
118
|
+
if (!isTTY || !sessionActive) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
126
121
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
122
|
+
yield* terminal.writeStderr("\n" + SHOW_CURSOR);
|
|
123
|
+
previousLineCount = 0;
|
|
124
|
+
sessionActive = false;
|
|
125
|
+
});
|
|
131
126
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
127
|
+
const renderNonTTYTaskUpdates = (
|
|
128
|
+
ordered: ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }>,
|
|
129
|
+
taskLines: ReadonlyArray<string>,
|
|
130
|
+
) => {
|
|
131
|
+
const nextTaskSignatureById = new Map<number, string>();
|
|
132
|
+
const changedTaskLines: Array<string> = [];
|
|
133
|
+
const nonTtyUpdateStep = Math.max(1, Math.floor(rendererConfig.nonTtyUpdateStep));
|
|
134
|
+
|
|
135
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
136
|
+
const taskId = ordered[i]!.snapshot.id as number;
|
|
137
|
+
const snapshot = ordered[i]!.snapshot;
|
|
138
|
+
const line = taskLines[i]!;
|
|
139
|
+
const signature =
|
|
140
|
+
snapshot.units._tag === "DeterminateTaskUnits"
|
|
141
|
+
? `${snapshot.status}:${snapshot.description}:${Math.floor(snapshot.units.completed / nonTtyUpdateStep)}:${snapshot.units.total}`
|
|
142
|
+
: `${snapshot.status}:${snapshot.description}`;
|
|
143
|
+
|
|
144
|
+
nextTaskSignatureById.set(taskId, signature);
|
|
145
|
+
if (nonTTYTaskSignatureById.get(taskId) !== signature) {
|
|
146
|
+
changedTaskLines.push(line);
|
|
147
|
+
}
|
|
152
148
|
}
|
|
153
|
-
}
|
|
154
149
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
150
|
+
return Effect.gen(function* () {
|
|
151
|
+
if (changedTaskLines.length > 0) {
|
|
152
|
+
yield* terminal.writeStderr(changedTaskLines.join("\n") + "\n");
|
|
153
|
+
}
|
|
159
154
|
|
|
160
|
-
|
|
161
|
-
});
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
const renderFrame = (mode: "tick" | "final") =>
|
|
165
|
-
Effect.gen(function* () {
|
|
166
|
-
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
167
|
-
const store = yield* Ref.get(storeRef);
|
|
168
|
-
const ordered = store.renderOrder.flatMap((row) => {
|
|
169
|
-
const snapshot = store.tasks.get(row.id);
|
|
170
|
-
if (!snapshot || (snapshot.transient && snapshot.status !== "running")) return [];
|
|
171
|
-
return [{ snapshot, depth: row.depth }];
|
|
172
|
-
});
|
|
173
|
-
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
174
|
-
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
175
|
-
const lineTick = isTTY ? frameTick : 0;
|
|
176
|
-
return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.config));
|
|
155
|
+
nonTTYTaskSignatureById = nextTaskSignatureById;
|
|
177
156
|
});
|
|
157
|
+
};
|
|
178
158
|
|
|
179
|
-
|
|
180
|
-
|
|
159
|
+
const renderFrame = (mode: "tick" | "final") =>
|
|
160
|
+
Effect.gen(function* () {
|
|
161
|
+
const drainedLogs = yield* Ref.getAndSet(pendingLogsRef, []);
|
|
162
|
+
const store = yield* Ref.get(storeRef);
|
|
163
|
+
const ordered = store.renderOrder.flatMap((row) => {
|
|
164
|
+
const snapshot = store.tasks.get(row.id);
|
|
165
|
+
if (!snapshot || (snapshot.transient && snapshot.status !== "running")) return [];
|
|
166
|
+
return [{ snapshot, depth: row.depth }];
|
|
167
|
+
});
|
|
168
|
+
const now = yield* Clock.currentTimeMillis;
|
|
169
|
+
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
170
|
+
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
171
|
+
const lineTick = isTTY ? frameTick : 0;
|
|
172
|
+
const taskColorizer = store.colorizers.get(snapshot.id) ?? colorizer;
|
|
173
|
+
return buildTaskLine(snapshot, depth, lineTick, taskColorizer, now);
|
|
174
|
+
});
|
|
181
175
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
176
|
+
if (isTTY) {
|
|
177
|
+
let frame = "";
|
|
178
|
+
|
|
179
|
+
// 1. Cursor reset — move up and clear previous frame lines
|
|
180
|
+
if (previousLineCount > 0) {
|
|
181
|
+
frame += "\r" + CLEAR_LINE;
|
|
182
|
+
for (let i = 1; i < previousLineCount; i++) {
|
|
183
|
+
frame += MOVE_UP_ONE + CLEAR_LINE;
|
|
184
|
+
}
|
|
187
185
|
}
|
|
188
|
-
}
|
|
189
186
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
187
|
+
if (retainLogHistory) {
|
|
188
|
+
const historyLogs = yield* Ref.get(logsRef);
|
|
189
|
+
const lines = yield* clipTTYFrameLines([...historyLogs, ...taskLines]);
|
|
190
|
+
if (lines.length > 0) {
|
|
191
|
+
frame += lines.join("\n");
|
|
192
|
+
}
|
|
193
|
+
previousLineCount = lines.length;
|
|
194
|
+
} else {
|
|
195
|
+
// 2. Logs (scroll above the task block)
|
|
196
|
+
if (drainedLogs.length > 0) {
|
|
197
|
+
frame += drainedLogs.join("\n") + "\n";
|
|
198
|
+
}
|
|
199
|
+
// 3. Task lines
|
|
200
|
+
if (taskLines.length > 0) {
|
|
201
|
+
frame += taskLines.join("\n");
|
|
202
|
+
}
|
|
203
|
+
previousLineCount = taskLines.length;
|
|
201
204
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
+
|
|
206
|
+
// 4. Single atomic write
|
|
207
|
+
if (frame) {
|
|
208
|
+
yield* terminal.writeStderr(frame);
|
|
205
209
|
}
|
|
206
|
-
|
|
210
|
+
return;
|
|
207
211
|
}
|
|
208
212
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
yield* terminal.writeStderr(frame);
|
|
213
|
+
if (drainedLogs.length > 0) {
|
|
214
|
+
yield* terminal.writeStderr(drainedLogs.join("\n") + "\n");
|
|
212
215
|
}
|
|
213
|
-
|
|
214
|
-
}
|
|
216
|
+
yield* renderNonTTYTaskUpdates(ordered, taskLines);
|
|
217
|
+
});
|
|
215
218
|
|
|
216
|
-
|
|
217
|
-
|
|
219
|
+
const renderLoop = Effect.gen(function* () {
|
|
220
|
+
rendererActive = true;
|
|
221
|
+
if (isTTY) {
|
|
222
|
+
yield* startTTYSession;
|
|
218
223
|
}
|
|
219
|
-
yield* renderNonTTYTaskUpdates(ordered, taskLines);
|
|
220
|
-
});
|
|
221
|
-
|
|
222
|
-
const renderLoop = Effect.gen(function* () {
|
|
223
|
-
rendererActive = true;
|
|
224
|
-
if (isTTY) {
|
|
225
|
-
yield* startTTYSession;
|
|
226
|
-
}
|
|
227
224
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
225
|
+
while (true) {
|
|
226
|
+
const dirty = yield* Ref.getAndSet(dirtyRef, false);
|
|
227
|
+
const tasks = Array.from((yield* Ref.get(storeRef)).tasks.values()).filter(
|
|
228
|
+
(task) => !(task.transient && task.status !== "running"),
|
|
229
|
+
);
|
|
230
|
+
const hasActiveSpinners = tasks.some(
|
|
231
|
+
(task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits",
|
|
232
|
+
);
|
|
233
|
+
const hasPendingLogs = (yield* Ref.get(pendingLogsRef)).length > 0;
|
|
237
234
|
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
if (isTTY) {
|
|
236
|
+
if (dirty || hasActiveSpinners || hasPendingLogs) {
|
|
237
|
+
yield* renderFrame("tick");
|
|
238
|
+
}
|
|
239
|
+
} else if (dirty || hasActiveSpinners) {
|
|
240
240
|
yield* renderFrame("tick");
|
|
241
241
|
}
|
|
242
|
-
} else if (dirty || hasActiveSpinners) {
|
|
243
|
-
yield* renderFrame("tick");
|
|
244
|
-
}
|
|
245
242
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
243
|
+
tick += 1;
|
|
244
|
+
yield* Effect.sleep(Math.max(1, Math.floor(rendererConfig.renderIntervalMillis)));
|
|
245
|
+
}
|
|
246
|
+
}).pipe(
|
|
247
|
+
Effect.ensuring(
|
|
248
|
+
Effect.gen(function* () {
|
|
249
|
+
if (!rendererActive) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
255
252
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
253
|
+
if (isTTY) {
|
|
254
|
+
if (sessionActive) {
|
|
255
|
+
yield* renderFrame("final");
|
|
256
|
+
yield* stopTTYSession;
|
|
257
|
+
}
|
|
258
|
+
return;
|
|
260
259
|
}
|
|
261
|
-
return;
|
|
262
|
-
}
|
|
263
260
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
261
|
+
yield* renderFrame("final");
|
|
262
|
+
}),
|
|
263
|
+
),
|
|
264
|
+
);
|
|
268
265
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
266
|
+
if (isTTY && rendererConfig.disableUserInput) {
|
|
267
|
+
return yield* terminal.withRawInputCapture(renderLoop);
|
|
268
|
+
}
|
|
272
269
|
|
|
273
|
-
|
|
274
|
-
};
|
|
270
|
+
return yield* renderLoop;
|
|
271
|
+
});
|
package/src/runtime.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
|
|
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
4
|
import { formatWithOptions } from "node:util";
|
|
5
5
|
import type { PartialDeep } from "type-fest";
|
|
6
|
+
import { Colorizer, type ColorizerService } from "./colors";
|
|
6
7
|
import { makeProgressConsole } from "./console";
|
|
7
8
|
import { runProgressServiceRenderer } from "./renderer";
|
|
8
9
|
import { ProgressTerminal } from "./terminal";
|
|
@@ -82,6 +83,8 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
82
83
|
transient: options.transient ?? snapshot.transient,
|
|
83
84
|
units,
|
|
84
85
|
config: snapshot.config,
|
|
86
|
+
startedAt: snapshot.startedAt,
|
|
87
|
+
completedAt: snapshot.completedAt,
|
|
85
88
|
});
|
|
86
89
|
};
|
|
87
90
|
|
|
@@ -117,7 +120,6 @@ const removeFromRenderOrder = (
|
|
|
117
120
|
const makeProgressService = Effect.gen(function* () {
|
|
118
121
|
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
119
122
|
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
120
|
-
|
|
121
123
|
const rendererConfig = decodeRendererConfigSync(
|
|
122
124
|
mergeConfig(
|
|
123
125
|
defaultRendererConfig,
|
|
@@ -138,6 +140,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
138
140
|
const storeRef = yield* Ref.make<TaskStore>({
|
|
139
141
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
140
142
|
renderOrder: [],
|
|
143
|
+
colorizers: new Map<TaskId, ColorizerService>(),
|
|
141
144
|
});
|
|
142
145
|
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
143
146
|
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
@@ -167,6 +170,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
167
170
|
options.parentId === undefined
|
|
168
171
|
? yield* FiberRef.get(currentParentRef)
|
|
169
172
|
: Option.some(options.parentId);
|
|
173
|
+
const colorizerOption = yield* Effect.serviceOption(Colorizer);
|
|
170
174
|
const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
|
|
171
175
|
const units =
|
|
172
176
|
options.total === undefined || options.total <= 0
|
|
@@ -181,6 +185,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
181
185
|
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
182
186
|
);
|
|
183
187
|
|
|
188
|
+
const now = yield* Clock.currentTimeMillis;
|
|
184
189
|
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
185
190
|
const snapshot = new TaskSnapshot({
|
|
186
191
|
id: taskId,
|
|
@@ -190,6 +195,8 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
190
195
|
transient: options.transient ?? false,
|
|
191
196
|
units,
|
|
192
197
|
config: resolvedProgressBarConfig,
|
|
198
|
+
startedAt: now,
|
|
199
|
+
completedAt: null,
|
|
193
200
|
});
|
|
194
201
|
|
|
195
202
|
yield* Ref.update(storeRef, (s) => {
|
|
@@ -198,7 +205,11 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
198
205
|
const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
|
|
199
206
|
const nextOrder = [...s.renderOrder];
|
|
200
207
|
nextOrder.splice(index, 0, { id: taskId, depth });
|
|
201
|
-
|
|
208
|
+
const nextColorizers = new Map(s.colorizers);
|
|
209
|
+
if (Option.isSome(colorizerOption)) {
|
|
210
|
+
nextColorizers.set(taskId, colorizerOption.value);
|
|
211
|
+
}
|
|
212
|
+
return { tasks: nextTasks, renderOrder: nextOrder, colorizers: nextColorizers };
|
|
202
213
|
});
|
|
203
214
|
yield* markDirty;
|
|
204
215
|
|
|
@@ -211,7 +222,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
211
222
|
if (!snapshot) return store;
|
|
212
223
|
const nextTasks = new Map(store.tasks);
|
|
213
224
|
nextTasks.set(taskId, updatedSnapshot(snapshot, options));
|
|
214
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
225
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, colorizers: store.colorizers };
|
|
215
226
|
}).pipe(Effect.zipRight(markDirty));
|
|
216
227
|
|
|
217
228
|
const advanceTask = (taskId: TaskId, amount = 1) =>
|
|
@@ -240,69 +251,95 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
240
251
|
transient: snapshot.transient,
|
|
241
252
|
units,
|
|
242
253
|
config: snapshot.config,
|
|
254
|
+
startedAt: snapshot.startedAt,
|
|
255
|
+
completedAt: snapshot.completedAt,
|
|
243
256
|
}),
|
|
244
257
|
);
|
|
245
258
|
|
|
246
|
-
return { tasks: nextTasks, renderOrder: store.renderOrder };
|
|
259
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, colorizers: store.colorizers };
|
|
247
260
|
}).pipe(Effect.zipRight(markDirty));
|
|
248
261
|
|
|
249
262
|
const completeTask = (taskId: TaskId) =>
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
nextTasks.
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
263
|
+
Effect.gen(function* () {
|
|
264
|
+
const now = yield* Clock.currentTimeMillis;
|
|
265
|
+
yield* Ref.update(storeRef, (store) => {
|
|
266
|
+
const snapshot = store.tasks.get(taskId);
|
|
267
|
+
if (!snapshot) return store;
|
|
268
|
+
|
|
269
|
+
const nextTasks = new Map(store.tasks);
|
|
270
|
+
if (snapshot.transient) {
|
|
271
|
+
nextTasks.delete(taskId);
|
|
272
|
+
const nextColorizers = new Map(store.colorizers);
|
|
273
|
+
nextColorizers.delete(taskId);
|
|
274
|
+
return {
|
|
275
|
+
tasks: nextTasks,
|
|
276
|
+
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
277
|
+
colorizers: nextColorizers,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
nextTasks.set(
|
|
282
|
+
taskId,
|
|
283
|
+
new TaskSnapshot({
|
|
284
|
+
id: snapshot.id,
|
|
285
|
+
parentId: snapshot.parentId,
|
|
286
|
+
description: snapshot.description,
|
|
287
|
+
status: "done",
|
|
288
|
+
transient: snapshot.transient,
|
|
289
|
+
units:
|
|
290
|
+
snapshot.units._tag === "DeterminateTaskUnits"
|
|
291
|
+
? new DeterminateTaskUnits({
|
|
292
|
+
completed: snapshot.units.total,
|
|
293
|
+
total: snapshot.units.total,
|
|
294
|
+
})
|
|
295
|
+
: snapshot.units,
|
|
296
|
+
config: snapshot.config,
|
|
297
|
+
startedAt: snapshot.startedAt,
|
|
298
|
+
completedAt: now,
|
|
299
|
+
}),
|
|
300
|
+
);
|
|
301
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, colorizers: store.colorizers };
|
|
302
|
+
});
|
|
303
|
+
yield* markDirty;
|
|
304
|
+
});
|
|
280
305
|
|
|
281
306
|
const failTask = (taskId: TaskId) =>
|
|
282
|
-
|
|
283
|
-
const
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
nextTasks.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
307
|
+
Effect.gen(function* () {
|
|
308
|
+
const now = yield* Clock.currentTimeMillis;
|
|
309
|
+
yield* Ref.update(storeRef, (store) => {
|
|
310
|
+
const snapshot = store.tasks.get(taskId);
|
|
311
|
+
if (!snapshot) return store;
|
|
312
|
+
|
|
313
|
+
const nextTasks = new Map(store.tasks);
|
|
314
|
+
if (snapshot.transient) {
|
|
315
|
+
nextTasks.delete(taskId);
|
|
316
|
+
const nextColorizers = new Map(store.colorizers);
|
|
317
|
+
nextColorizers.delete(taskId);
|
|
318
|
+
return {
|
|
319
|
+
tasks: nextTasks,
|
|
320
|
+
renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
|
|
321
|
+
colorizers: nextColorizers,
|
|
322
|
+
};
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
nextTasks.set(
|
|
326
|
+
taskId,
|
|
327
|
+
new TaskSnapshot({
|
|
328
|
+
id: snapshot.id,
|
|
329
|
+
parentId: snapshot.parentId,
|
|
330
|
+
description: snapshot.description,
|
|
331
|
+
status: "failed",
|
|
332
|
+
transient: snapshot.transient,
|
|
333
|
+
units: snapshot.units,
|
|
334
|
+
config: snapshot.config,
|
|
335
|
+
startedAt: snapshot.startedAt,
|
|
336
|
+
completedAt: now,
|
|
337
|
+
}),
|
|
338
|
+
);
|
|
339
|
+
return { tasks: nextTasks, renderOrder: store.renderOrder, colorizers: store.colorizers };
|
|
340
|
+
});
|
|
341
|
+
yield* markDirty;
|
|
342
|
+
});
|
|
306
343
|
|
|
307
344
|
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
308
345
|
Effect.gen(function* () {
|
|
@@ -405,22 +442,19 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
405
442
|
return Progress.of(service);
|
|
406
443
|
});
|
|
407
444
|
|
|
408
|
-
export class Progress extends Context.Tag("stromseng.dev/Progress")<
|
|
409
|
-
|
|
445
|
+
export class Progress extends Context.Tag("stromseng.dev/effective-progress/Progress")<
|
|
446
|
+
Progress,
|
|
447
|
+
ProgressService
|
|
448
|
+
>() {
|
|
449
|
+
static readonly Default = Layer.unwrapEffect(
|
|
450
|
+
Effect.gen(function* () {
|
|
451
|
+
const colorizerOption = yield* Effect.serviceOption(Colorizer);
|
|
452
|
+
const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
|
|
453
|
+
const base = Layer.scoped(Progress, makeProgressService);
|
|
454
|
+
return base.pipe(
|
|
455
|
+
Option.isNone(colorizerOption) ? Layer.provide(Colorizer.Default) : (l) => l,
|
|
456
|
+
Option.isNone(terminalOption) ? Layer.provide(ProgressTerminal.Default) : (l) => l,
|
|
457
|
+
);
|
|
458
|
+
}),
|
|
459
|
+
);
|
|
410
460
|
}
|
|
411
|
-
|
|
412
|
-
export const provideProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
413
|
-
Effect.gen(function* () {
|
|
414
|
-
const existing = yield* Effect.serviceOption(Progress);
|
|
415
|
-
if (Option.isSome(existing)) {
|
|
416
|
-
return yield* Effect.provideService(effect, Progress, existing.value);
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
const existingTerminal = yield* Effect.serviceOption(ProgressTerminal);
|
|
420
|
-
if (Option.isSome(existingTerminal)) {
|
|
421
|
-
return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
|
|
422
|
-
}
|
|
423
|
-
|
|
424
|
-
const defaultLayers = Layer.provide(Progress.Default, ProgressTerminal.Default);
|
|
425
|
-
return yield* Effect.scoped(effect.pipe(Effect.provide(defaultLayers)));
|
|
426
|
-
});
|
package/src/terminal.ts
CHANGED
|
@@ -42,18 +42,20 @@ const withRawInputCapture: ProgressTerminalService["withRawInputCapture"] = (eff
|
|
|
42
42
|
);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
|
+
const defaultTerminalService: ProgressTerminalService = {
|
|
46
|
+
isTTY: Effect.sync(() => Boolean(process.stderr.isTTY)),
|
|
47
|
+
stderrRows: Effect.sync(() => process.stderr.rows),
|
|
48
|
+
stderrColumns: Effect.sync(() => process.stderr.columns),
|
|
49
|
+
writeStderr: (text) =>
|
|
50
|
+
Effect.sync(() => {
|
|
51
|
+
process.stderr.write(text);
|
|
52
|
+
}),
|
|
53
|
+
withRawInputCapture,
|
|
54
|
+
};
|
|
55
|
+
|
|
45
56
|
export class ProgressTerminal extends Context.Tag("stromseng.dev/ProgressTerminal")<
|
|
46
57
|
ProgressTerminal,
|
|
47
58
|
ProgressTerminalService
|
|
48
59
|
>() {
|
|
49
|
-
static readonly Default = Layer.succeed(ProgressTerminal,
|
|
50
|
-
isTTY: Effect.sync(() => Boolean(process.stderr.isTTY)),
|
|
51
|
-
stderrRows: Effect.sync(() => process.stderr.rows),
|
|
52
|
-
stderrColumns: Effect.sync(() => process.stderr.columns),
|
|
53
|
-
writeStderr: (text) =>
|
|
54
|
-
Effect.sync(() => {
|
|
55
|
-
process.stderr.write(text);
|
|
56
|
-
}),
|
|
57
|
-
withRawInputCapture,
|
|
58
|
-
} satisfies ProgressTerminalService);
|
|
60
|
+
static readonly Default = Layer.succeed(ProgressTerminal, defaultTerminalService);
|
|
59
61
|
}
|
package/src/types.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Brand, Context, Effect, Option, Schema } from "effect";
|
|
2
2
|
import type { PartialDeep } from "type-fest";
|
|
3
|
-
import {
|
|
3
|
+
import type { ColorizerService } from "./colors";
|
|
4
4
|
|
|
5
5
|
export const RendererConfigSchema = Schema.Struct({
|
|
6
6
|
disableUserInput: Schema.Boolean,
|
|
@@ -18,14 +18,13 @@ export const ProgressBarConfigSchema = Schema.Struct({
|
|
|
18
18
|
emptyChar: Schema.String,
|
|
19
19
|
leftBracket: Schema.String,
|
|
20
20
|
rightBracket: Schema.String,
|
|
21
|
-
colors: ProgressBarColorsSchema,
|
|
22
21
|
});
|
|
23
22
|
export type ProgressBarConfigShape = typeof ProgressBarConfigSchema.Type;
|
|
24
23
|
export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarConfigSchema);
|
|
25
24
|
|
|
26
25
|
export const defaultRendererConfig: RendererConfigShape = {
|
|
27
26
|
disableUserInput: true,
|
|
28
|
-
renderIntervalMillis:
|
|
27
|
+
renderIntervalMillis: 100, // 10 FPS
|
|
29
28
|
maxLogLines: 0,
|
|
30
29
|
nonTtyUpdateStep: 5,
|
|
31
30
|
};
|
|
@@ -37,18 +36,16 @@ export const defaultProgressBarConfig: ProgressBarConfigShape = {
|
|
|
37
36
|
emptyChar: "─",
|
|
38
37
|
leftBracket: "",
|
|
39
38
|
rightBracket: "",
|
|
40
|
-
colors: defaultProgressBarColors,
|
|
41
39
|
};
|
|
42
40
|
|
|
43
|
-
export class RendererConfig extends Context.Tag("stromseng.dev/RendererConfig")<
|
|
41
|
+
export class RendererConfig extends Context.Tag("stromseng.dev/effective-progress/RendererConfig")<
|
|
44
42
|
RendererConfig,
|
|
45
43
|
PartialDeep<RendererConfigShape>
|
|
46
44
|
>() {}
|
|
47
45
|
|
|
48
|
-
export class ProgressBarConfig extends Context.Tag(
|
|
49
|
-
ProgressBarConfig,
|
|
50
|
-
|
|
51
|
-
>() {}
|
|
46
|
+
export class ProgressBarConfig extends Context.Tag(
|
|
47
|
+
"stromseng.dev/effective-progress/ProgressBarConfig",
|
|
48
|
+
)<ProgressBarConfig, PartialDeep<ProgressBarConfigShape>>() {}
|
|
52
49
|
|
|
53
50
|
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
54
51
|
|
|
@@ -103,6 +100,8 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
|
|
|
103
100
|
transient: Schema.Boolean,
|
|
104
101
|
units: TaskUnitsSchema,
|
|
105
102
|
config: ProgressBarConfigSchema,
|
|
103
|
+
startedAt: Schema.Number,
|
|
104
|
+
completedAt: Schema.NullOr(Schema.Number),
|
|
106
105
|
}) {}
|
|
107
106
|
|
|
108
107
|
export interface RenderRow {
|
|
@@ -113,6 +112,7 @@ export interface RenderRow {
|
|
|
113
112
|
export interface TaskStore {
|
|
114
113
|
readonly tasks: Map<TaskId, TaskSnapshot>;
|
|
115
114
|
readonly renderOrder: ReadonlyArray<RenderRow>;
|
|
115
|
+
readonly colorizers: Map<TaskId, ColorizerService>;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
export interface ProgressService {
|
|
@@ -144,7 +144,7 @@ export interface ProgressService {
|
|
|
144
144
|
};
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
export class Task extends Context.Tag("stromseng.dev/Task")<Task, TaskId>() {}
|
|
147
|
+
export class Task extends Context.Tag("stromseng.dev/effective-progress/Task")<Task, TaskId>() {}
|
|
148
148
|
|
|
149
149
|
export class TaskAddedEvent extends Schema.TaggedClass<TaskAddedEvent>()("TaskAdded", {
|
|
150
150
|
taskId: TaskIdSchema,
|