effective-progress 0.1.2 → 0.1.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 +119 -15
- package/package.json +4 -2
- package/src/api.ts +15 -15
- package/src/colors.ts +206 -0
- package/src/console.ts +3 -4
- package/src/index.ts +1 -0
- package/src/renderer.ts +53 -21
- package/src/runtime.ts +85 -11
- package/src/types.ts +32 -13
package/README.md
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
# effective-progress
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/effective-progress)
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> Pre-`1.0.0`, breaking changes may happen in any release. SemVer guarantees will begin at `1.0.0`.
|
|
7
|
+
> I recommend using only the `Progress.all` and `Progress.forEach` APIs for now, as they will likely change the least. The lower-level APIs for manual progress bar control are more likely to see breaking changes as I iterate on the design.
|
|
8
|
+
>
|
|
9
|
+
> Please open an issue or reach out if you have any questions or want to contribute!
|
|
10
|
+
> Feedback and contributions are very welcome!
|
|
11
|
+
|
|
12
|
+
<img alt="Showcase output" src="docs/images/showcase.gif" width="600" />
|
|
13
|
+
|
|
3
14
|
`effective-progress` is an [Effect](https://effect.website/)-first terminal progress library with:
|
|
4
15
|
|
|
5
16
|
- multiple progress bars
|
|
6
17
|
- nested child progress bars
|
|
7
18
|
- spinner support for indeterminate work
|
|
8
19
|
- clean log rendering alongside progress output, allowing you to simply use Effects `Console.log` or `Effect.logInfo`.
|
|
20
|
+
- simple to use `.all` and `.forEach` APIs similar to the ones you already know and love from `effect`. Just swap `Effect` for `Progress` and get progress bars for free!
|
|
9
21
|
|
|
10
22
|
## Install
|
|
11
23
|
|
|
@@ -32,12 +44,7 @@ const program = Progress.all(
|
|
|
32
44
|
Effect.runPromise(program);
|
|
33
45
|
```
|
|
34
46
|
|
|
35
|
-
|
|
36
|
-
bun run examples/basic.ts
|
|
37
|
-
Completed task 1
|
|
38
|
-
Completed task 2
|
|
39
|
-
- Running tasks in parallel: ━━━━━━━━━━━━────────────────── 2/5 40%
|
|
40
|
-
```
|
|
47
|
+
<img alt="Basic example output" src="docs/images/basic.gif" width="600" />
|
|
41
48
|
|
|
42
49
|
## Nested example
|
|
43
50
|
|
|
@@ -68,25 +75,122 @@ const program = Progress.all(
|
|
|
68
75
|
Effect.runPromise(program);
|
|
69
76
|
```
|
|
70
77
|
|
|
71
|
-
|
|
72
|
-
❯ bun run examples/nesting.ts
|
|
73
|
-
- Running tasks in parallel: ━━━━━━━━━━━━────────────────── 2/5 40%
|
|
74
|
-
- Running subtasks for task 3: ━━━━━━━━────────────────────── 4/15 27%
|
|
75
|
-
- Running subtasks for task 4: ━━━━━━━━────────────────────── 4/15 27%
|
|
76
|
-
```
|
|
78
|
+
<img alt="Nested example output" src="docs/images/nesting.gif" width="600" />
|
|
77
79
|
|
|
78
80
|
## Other examples
|
|
79
81
|
|
|
80
82
|
- `examples/simpleExample.ts` - low-boilerplate real-world flow
|
|
81
83
|
- `examples/advancedExample.ts` - full API usage with custom config and manual task control
|
|
84
|
+
- `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
|
|
82
85
|
|
|
83
86
|
## Log retention
|
|
84
87
|
|
|
85
|
-
- `maxLogLines` controls in-memory log retention.
|
|
86
|
-
-
|
|
88
|
+
- `maxLogLines` on `RendererConfig` controls in-memory log retention.
|
|
89
|
+
- Omitted or set to `0` means no log history is kept in memory.
|
|
87
90
|
- `maxLogLines > 0` keeps only the latest `N` log lines in memory.
|
|
88
91
|
|
|
92
|
+
## Configuring renderer and progress bars
|
|
93
|
+
|
|
94
|
+
Configure global renderer behavior once, and a global base progress bar style:
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
import { Effect } from "effect";
|
|
98
|
+
import * as Progress from "effective-progress";
|
|
99
|
+
|
|
100
|
+
const configured = program.pipe(
|
|
101
|
+
Effect.provideService(Progress.RendererConfig, {
|
|
102
|
+
maxLogLines: 12,
|
|
103
|
+
nonTtyUpdateStep: 2,
|
|
104
|
+
}),
|
|
105
|
+
Effect.provideService(Progress.ProgressBarConfig, {
|
|
106
|
+
barWidth: 36,
|
|
107
|
+
colors: {
|
|
108
|
+
fill: { kind: "hex", value: "#00b894" },
|
|
109
|
+
spinner: { kind: "ansi256", value: 214 },
|
|
110
|
+
},
|
|
111
|
+
}),
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
Effect.runPromise(configured);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Task-level `progressbar` config is optional and inherits from its parent task (or from global `ProgressBarConfig` for root tasks):
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
yield *
|
|
121
|
+
progress.withTask(
|
|
122
|
+
{
|
|
123
|
+
description: "Worker pipeline",
|
|
124
|
+
progressbar: {
|
|
125
|
+
barWidth: 20,
|
|
126
|
+
spinnerFrames: [".", "o", "O", "0"],
|
|
127
|
+
colors: {
|
|
128
|
+
spinner: { kind: "named", value: "magentaBright" },
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
() => Effect.sleep("1 second"),
|
|
133
|
+
);
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
For manual service usage, capture logs explicitly:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
const program = Progress.provide(
|
|
140
|
+
Effect.gen(function* () {
|
|
141
|
+
const progress = yield* Progress.Progress;
|
|
142
|
+
|
|
143
|
+
yield* progress.withTask({ description: "Manual task" }, () =>
|
|
144
|
+
progress.withCapturedLogs(
|
|
145
|
+
Effect.gen(function* () {
|
|
146
|
+
yield* Console.log("This log is rendered through progress output");
|
|
147
|
+
yield* Effect.sleep("1 second");
|
|
148
|
+
}),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
}),
|
|
152
|
+
);
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Progress bar colors
|
|
156
|
+
|
|
157
|
+
`progressbar.colors` is configured with typed color tokens that are validated by Effect Schema.
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
progressbar: {
|
|
161
|
+
spinnerFrames: ["-", "\\", "|", "/"],
|
|
162
|
+
barWidth: 30,
|
|
163
|
+
fillChar: "━",
|
|
164
|
+
emptyChar: "─",
|
|
165
|
+
leftBracket: "",
|
|
166
|
+
rightBracket: "",
|
|
167
|
+
colors: {
|
|
168
|
+
fill: { kind: "named", value: "cyan" },
|
|
169
|
+
empty: { kind: "hex", value: "#9ca3af", modifiers: ["dim"] },
|
|
170
|
+
brackets: { kind: "rgb", value: { r: 156, g: 163, b: 175 } },
|
|
171
|
+
percent: { kind: "named", value: "whiteBright", modifiers: ["bold"] },
|
|
172
|
+
spinner: { kind: "ansi256", value: 214 },
|
|
173
|
+
done: { kind: "named", value: "greenBright" },
|
|
174
|
+
failed: { kind: "named", value: "redBright", modifiers: ["bold"] },
|
|
175
|
+
},
|
|
176
|
+
}
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
Supported color styles:
|
|
180
|
+
|
|
181
|
+
- `named` (for example `cyan`, `greenBright`)
|
|
182
|
+
- `hex` (for example `#00b894`)
|
|
183
|
+
- `rgb` (for example `{ r: 0, g: 184, b: 148 }`)
|
|
184
|
+
- `ansi256` (for example `214`)
|
|
185
|
+
|
|
186
|
+
Supported modifiers:
|
|
187
|
+
|
|
188
|
+
- `bold`, `dim`, `italic`, `underline`, `inverse`, `hidden`, `strikethrough`
|
|
189
|
+
|
|
190
|
+
## Dependencies & package size
|
|
191
|
+
|
|
192
|
+
This library is designed for CLI workflows, where package size is typically a lower-priority concern. Alongside `effect`, it relies on two mature runtime dependencies: `chalk` and `es-toolkit`.
|
|
193
|
+
|
|
89
194
|
## Notes
|
|
90
195
|
|
|
91
|
-
- This is a WIP library, so expect breaking changes. Feedback and contributions are very welcome!
|
|
92
196
|
- As Effect 4.0 is around the corner with some changes to logging, there may be some adjustments needed to align with the new Effect APIs.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effective-progress",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.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": {
|
|
@@ -31,7 +31,9 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"chalk": "^5.6.2",
|
|
34
|
-
"effect": "^3.19.16"
|
|
34
|
+
"effect": "^3.19.16",
|
|
35
|
+
"es-toolkit": "^1.44.0",
|
|
36
|
+
"type-fest": "^5.4.4"
|
|
35
37
|
},
|
|
36
38
|
"devDependencies": {
|
|
37
39
|
"@effect/language-service": "^0.73.1",
|
package/src/api.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Effect, Option } from "effect";
|
|
2
2
|
import type { Concurrency } from "effect/Types";
|
|
3
|
-
import { makeProgressConsole } from "./console";
|
|
4
3
|
import { makeProgressService } from "./runtime";
|
|
5
4
|
import { Progress } from "./types";
|
|
6
5
|
import type { TrackOptions } from "./types";
|
|
@@ -32,23 +31,17 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
|
32
31
|
|
|
33
32
|
export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
34
33
|
|
|
35
|
-
export const
|
|
34
|
+
export const provide = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
36
35
|
Effect.gen(function* () {
|
|
37
|
-
const outerConsole = yield* Console.consoleWith((console) => Effect.succeed(console));
|
|
38
36
|
const existing = yield* Effect.serviceOption(Progress);
|
|
39
37
|
if (Option.isSome(existing)) {
|
|
40
|
-
|
|
41
|
-
return yield* Effect.withConsole(
|
|
42
|
-
Effect.provideService(effect, Progress, existing.value),
|
|
43
|
-
console,
|
|
44
|
-
);
|
|
38
|
+
return yield* Effect.provideService(effect, Progress, existing.value);
|
|
45
39
|
}
|
|
46
40
|
|
|
47
41
|
return yield* Effect.scoped(
|
|
48
42
|
Effect.gen(function* () {
|
|
49
43
|
const service = yield* makeProgressService;
|
|
50
|
-
|
|
51
|
-
return yield* Effect.withConsole(Effect.provideService(effect, Progress, service), console);
|
|
44
|
+
return yield* Effect.provideService(effect, Progress, service);
|
|
52
45
|
}),
|
|
53
46
|
);
|
|
54
47
|
});
|
|
@@ -60,7 +53,7 @@ export const all = <
|
|
|
60
53
|
effects: Arg,
|
|
61
54
|
options: Omit<TrackOptions, "total"> & O,
|
|
62
55
|
): AllReturn<Arg, O> =>
|
|
63
|
-
|
|
56
|
+
provide(
|
|
64
57
|
Effect.gen(function* () {
|
|
65
58
|
const progress = yield* Progress;
|
|
66
59
|
return yield* progress.withTask(
|
|
@@ -68,10 +61,13 @@ export const all = <
|
|
|
68
61
|
description: options.description,
|
|
69
62
|
total: effects.length,
|
|
70
63
|
transient: options.transient,
|
|
64
|
+
progressbar: options.progressbar,
|
|
71
65
|
},
|
|
72
66
|
(taskId) =>
|
|
73
67
|
Effect.all(
|
|
74
|
-
effects.map((effect) =>
|
|
68
|
+
effects.map((effect) =>
|
|
69
|
+
Effect.tap(progress.withCapturedLogs(effect), () => progress.advanceTask(taskId, 1)),
|
|
70
|
+
),
|
|
75
71
|
{
|
|
76
72
|
concurrency: options.concurrency,
|
|
77
73
|
batching: options.batching,
|
|
@@ -89,7 +85,7 @@ export const forEach = <A, B, E, R>(
|
|
|
89
85
|
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
90
86
|
options: ForEachOptions,
|
|
91
87
|
): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress>> =>
|
|
92
|
-
|
|
88
|
+
provide(
|
|
93
89
|
Effect.gen(function* () {
|
|
94
90
|
const progress = yield* Progress;
|
|
95
91
|
|
|
@@ -98,11 +94,15 @@ export const forEach = <A, B, E, R>(
|
|
|
98
94
|
description: options.description,
|
|
99
95
|
total: options.total ?? inferTotal(iterable),
|
|
100
96
|
transient: options.transient,
|
|
97
|
+
progressbar: options.progressbar,
|
|
101
98
|
},
|
|
102
99
|
(taskId) =>
|
|
103
100
|
Effect.forEach(
|
|
104
101
|
iterable,
|
|
105
|
-
(item, index) =>
|
|
102
|
+
(item, index) =>
|
|
103
|
+
Effect.tap(progress.withCapturedLogs(f(item, index)), () =>
|
|
104
|
+
progress.advanceTask(taskId, 1),
|
|
105
|
+
),
|
|
106
106
|
{
|
|
107
107
|
concurrency: options.concurrency,
|
|
108
108
|
batching: options.batching,
|
package/src/colors.ts
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import type { ChalkInstance } from "chalk";
|
|
3
|
+
import { Schema } from "effect";
|
|
4
|
+
|
|
5
|
+
export const StyleModifierSchema = Schema.Literal(
|
|
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
|
+
switch (modifier) {
|
|
108
|
+
case "bold":
|
|
109
|
+
return instance.bold;
|
|
110
|
+
case "dim":
|
|
111
|
+
return instance.dim;
|
|
112
|
+
case "italic":
|
|
113
|
+
return instance.italic;
|
|
114
|
+
case "underline":
|
|
115
|
+
return instance.underline;
|
|
116
|
+
case "inverse":
|
|
117
|
+
return instance.inverse;
|
|
118
|
+
case "hidden":
|
|
119
|
+
return instance.hidden;
|
|
120
|
+
case "strikethrough":
|
|
121
|
+
return instance.strikethrough;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const applyNamedColor = (instance: ChalkInstance, color: NamedColor): ChalkInstance => {
|
|
126
|
+
switch (color) {
|
|
127
|
+
case "black":
|
|
128
|
+
return instance.black;
|
|
129
|
+
case "red":
|
|
130
|
+
return instance.red;
|
|
131
|
+
case "green":
|
|
132
|
+
return instance.green;
|
|
133
|
+
case "yellow":
|
|
134
|
+
return instance.yellow;
|
|
135
|
+
case "blue":
|
|
136
|
+
return instance.blue;
|
|
137
|
+
case "magenta":
|
|
138
|
+
return instance.magenta;
|
|
139
|
+
case "cyan":
|
|
140
|
+
return instance.cyan;
|
|
141
|
+
case "white":
|
|
142
|
+
return instance.white;
|
|
143
|
+
case "blackBright":
|
|
144
|
+
return instance.blackBright;
|
|
145
|
+
case "redBright":
|
|
146
|
+
return instance.redBright;
|
|
147
|
+
case "greenBright":
|
|
148
|
+
return instance.greenBright;
|
|
149
|
+
case "yellowBright":
|
|
150
|
+
return instance.yellowBright;
|
|
151
|
+
case "blueBright":
|
|
152
|
+
return instance.blueBright;
|
|
153
|
+
case "magentaBright":
|
|
154
|
+
return instance.magentaBright;
|
|
155
|
+
case "cyanBright":
|
|
156
|
+
return instance.cyanBright;
|
|
157
|
+
case "whiteBright":
|
|
158
|
+
return instance.whiteBright;
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
const resolveBaseStyle = (style: ColorStyle): ChalkInstance => {
|
|
163
|
+
switch (style.kind) {
|
|
164
|
+
case "named":
|
|
165
|
+
return applyNamedColor(chalk, style.value);
|
|
166
|
+
case "hex":
|
|
167
|
+
return chalk.hex(style.value);
|
|
168
|
+
case "rgb":
|
|
169
|
+
return chalk.rgb(style.value.r, style.value.g, style.value.b);
|
|
170
|
+
case "ansi256":
|
|
171
|
+
return chalk.ansi256(style.value);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const applyModifiers = (instance: ChalkInstance, modifiers: ReadonlyArray<StyleModifier>) => {
|
|
176
|
+
let styled = instance;
|
|
177
|
+
for (const modifier of modifiers) {
|
|
178
|
+
styled = applyModifier(styled, modifier);
|
|
179
|
+
}
|
|
180
|
+
return styled;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export const applyColorStyle = (style: ColorStyle, text: string): string => {
|
|
184
|
+
const styled = applyModifiers(resolveBaseStyle(style), style.modifiers ?? []);
|
|
185
|
+
return styled(text);
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export interface CompiledProgressBarColors {
|
|
189
|
+
readonly fill: (text: string) => string;
|
|
190
|
+
readonly empty: (text: string) => string;
|
|
191
|
+
readonly brackets: (text: string) => string;
|
|
192
|
+
readonly percent: (text: string) => string;
|
|
193
|
+
readonly spinner: (text: string) => string;
|
|
194
|
+
readonly done: (text: string) => string;
|
|
195
|
+
readonly failed: (text: string) => string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const compileProgressBarColors = (colors: ProgressBarColors): CompiledProgressBarColors => ({
|
|
199
|
+
fill: (text) => applyColorStyle(colors.fill, text),
|
|
200
|
+
empty: (text) => applyColorStyle(colors.empty, text),
|
|
201
|
+
brackets: (text) => applyColorStyle(colors.brackets, text),
|
|
202
|
+
percent: (text) => applyColorStyle(colors.percent, text),
|
|
203
|
+
spinner: (text) => applyColorStyle(colors.spinner, text),
|
|
204
|
+
done: (text) => applyColorStyle(colors.done, text),
|
|
205
|
+
failed: (text) => applyColorStyle(colors.failed, text),
|
|
206
|
+
});
|
package/src/console.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { Console, Effect } from "effect";
|
|
2
2
|
import { formatWithOptions } from "node:util";
|
|
3
|
-
import type { ProgressService } from "./types";
|
|
4
3
|
|
|
5
4
|
export const makeProgressConsole = (
|
|
6
|
-
|
|
5
|
+
progressLog: (...args: ReadonlyArray<unknown>) => Effect.Effect<void, never, never>,
|
|
7
6
|
outerConsole: Console.Console,
|
|
8
7
|
): Console.Console => {
|
|
9
|
-
const log = (...args: ReadonlyArray<unknown>) =>
|
|
8
|
+
const log = (...args: ReadonlyArray<unknown>) => progressLog(...args);
|
|
10
9
|
const unsafeLog = (...args: ReadonlyArray<unknown>) => {
|
|
11
|
-
Effect.runFork(
|
|
10
|
+
Effect.runFork(progressLog(...args));
|
|
12
11
|
};
|
|
13
12
|
|
|
14
13
|
const delegate = (effect: Effect.Effect<void, never, never>) => effect;
|
package/src/index.ts
CHANGED
package/src/renderer.ts
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
|
-
import { Effect, Ref } from "effect";
|
|
2
|
-
import
|
|
3
|
-
|
|
1
|
+
import { Effect, Ref, Schema } from "effect";
|
|
2
|
+
import {
|
|
3
|
+
type CompiledProgressBarColors,
|
|
4
|
+
compileProgressBarColors,
|
|
5
|
+
ProgressBarColorsSchema,
|
|
6
|
+
} from "./colors";
|
|
7
|
+
import type { ProgressBarConfigShape, RendererConfigShape } from "./types";
|
|
4
8
|
import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
|
|
5
9
|
|
|
6
10
|
const HIDE_CURSOR = "\x1b[?25l";
|
|
7
11
|
const SHOW_CURSOR = "\x1b[?25h";
|
|
8
12
|
const CLEAR_LINE = "\x1b[2K";
|
|
9
13
|
const MOVE_UP_ONE = "\x1b[1A";
|
|
10
|
-
const
|
|
14
|
+
const encodeProgressBarColorsKey = Schema.encodeSync(Schema.parseJson(ProgressBarColorsSchema));
|
|
11
15
|
|
|
12
|
-
const renderDeterminate = (
|
|
16
|
+
const renderDeterminate = (
|
|
17
|
+
units: DeterminateTaskUnits,
|
|
18
|
+
progressbar: ProgressBarConfigShape,
|
|
19
|
+
colors: CompiledProgressBarColors,
|
|
20
|
+
): string => {
|
|
13
21
|
const safeTotal = units.total <= 0 ? 1 : units.total;
|
|
14
22
|
const ratio = Math.min(1, Math.max(0, units.completed / safeTotal));
|
|
15
|
-
const filled = Math.round(ratio *
|
|
16
|
-
const bar = `${
|
|
23
|
+
const filled = Math.round(ratio * progressbar.barWidth);
|
|
24
|
+
const bar = `${colors.fill(progressbar.fillChar.repeat(filled))}${colors.empty(progressbar.emptyChar.repeat(progressbar.barWidth - filled))}`;
|
|
17
25
|
const percent = String(Math.round(ratio * 100)).padStart(3, " ");
|
|
18
|
-
return `${
|
|
26
|
+
return `${colors.brackets(progressbar.leftBracket)}${bar}${colors.brackets(progressbar.rightBracket)} ${units.completed}/${units.total} ${colors.percent(percent + "%")}`;
|
|
19
27
|
};
|
|
20
28
|
|
|
21
29
|
const buildTaskLine = (
|
|
22
30
|
snapshot: TaskSnapshot,
|
|
23
31
|
depth: number,
|
|
24
32
|
tick: number,
|
|
25
|
-
|
|
33
|
+
colors: CompiledProgressBarColors,
|
|
26
34
|
): string => {
|
|
35
|
+
const progressbar = snapshot.progressbar;
|
|
27
36
|
const prefix = `${" ".repeat(depth)}- ${snapshot.description}: `;
|
|
28
37
|
|
|
29
38
|
if (snapshot.status === "failed") {
|
|
30
|
-
return `${prefix}${
|
|
39
|
+
return `${prefix}${colors.failed("[failed]")}`;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
if (snapshot.status === "done") {
|
|
34
43
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
35
|
-
return `${prefix}${
|
|
44
|
+
return `${prefix}${colors.done("[done]")} ${snapshot.units.completed}/${snapshot.units.total}`;
|
|
36
45
|
}
|
|
37
|
-
return `${prefix}${
|
|
46
|
+
return `${prefix}${colors.done("[done]")}`;
|
|
38
47
|
}
|
|
39
48
|
|
|
40
49
|
if (snapshot.units._tag === "DeterminateTaskUnits") {
|
|
41
|
-
return prefix + renderDeterminate(snapshot.units,
|
|
50
|
+
return prefix + renderDeterminate(snapshot.units, progressbar, colors);
|
|
42
51
|
}
|
|
43
52
|
|
|
44
|
-
const frames =
|
|
53
|
+
const frames = progressbar.spinnerFrames;
|
|
45
54
|
const frameIndex = (snapshot.units.spinnerFrame + tick) % frames.length;
|
|
46
|
-
|
|
55
|
+
const frame = frames[frameIndex] ?? frames[0]!;
|
|
56
|
+
return `${prefix}${colors.spinner(frame)}`;
|
|
47
57
|
};
|
|
48
58
|
|
|
49
59
|
const orderTasksForRender = (
|
|
@@ -74,17 +84,32 @@ export const runProgressServiceRenderer = (
|
|
|
74
84
|
logsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
75
85
|
pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
76
86
|
dirtyRef: Ref.Ref<boolean>,
|
|
77
|
-
|
|
87
|
+
rendererConfig: RendererConfigShape,
|
|
78
88
|
maxRetainedLogLines: number,
|
|
89
|
+
rendererLatch: Effect.Latch,
|
|
79
90
|
) => {
|
|
80
|
-
const isTTY =
|
|
91
|
+
const isTTY = rendererConfig.isTTY;
|
|
81
92
|
const retainLogHistory = maxRetainedLogLines > 0;
|
|
93
|
+
const colorCache = new Map<string, CompiledProgressBarColors>();
|
|
82
94
|
let previousLineCount = 0;
|
|
83
95
|
let previousTaskLineCount = 0;
|
|
84
96
|
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
85
97
|
let tick = 0;
|
|
98
|
+
let rendererActive = false;
|
|
86
99
|
let teardownInput: (() => void) | undefined;
|
|
87
100
|
|
|
101
|
+
const getCompiledColors = (progressbar: ProgressBarConfigShape): CompiledProgressBarColors => {
|
|
102
|
+
const key = encodeProgressBarColorsKey(progressbar.colors);
|
|
103
|
+
const cached = colorCache.get(key);
|
|
104
|
+
if (cached) {
|
|
105
|
+
return cached;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const compiled = compileProgressBarColors(progressbar.colors);
|
|
109
|
+
colorCache.set(key, compiled);
|
|
110
|
+
return compiled;
|
|
111
|
+
};
|
|
112
|
+
|
|
88
113
|
const clearTTYLines = (lineCount: number) => {
|
|
89
114
|
if (lineCount <= 0) {
|
|
90
115
|
return;
|
|
@@ -103,7 +128,7 @@ export const runProgressServiceRenderer = (
|
|
|
103
128
|
) => {
|
|
104
129
|
const nextTaskSignatureById = new Map<number, string>();
|
|
105
130
|
const changedTaskLines: Array<string> = [];
|
|
106
|
-
const nonTtyUpdateStep = Math.max(1, Math.floor(
|
|
131
|
+
const nonTtyUpdateStep = Math.max(1, Math.floor(rendererConfig.nonTtyUpdateStep));
|
|
107
132
|
|
|
108
133
|
for (let i = 0; i < ordered.length; i++) {
|
|
109
134
|
const taskId = ordered[i]!.snapshot.id as number;
|
|
@@ -137,7 +162,7 @@ export const runProgressServiceRenderer = (
|
|
|
137
162
|
const frameTick = mode === "final" ? tick + 1 : tick;
|
|
138
163
|
const taskLines = ordered.map(({ snapshot, depth }) => {
|
|
139
164
|
const lineTick = isTTY ? frameTick : 0;
|
|
140
|
-
return buildTaskLine(snapshot, depth, lineTick,
|
|
165
|
+
return buildTaskLine(snapshot, depth, lineTick, getCompiledColors(snapshot.progressbar));
|
|
141
166
|
});
|
|
142
167
|
|
|
143
168
|
if (isTTY) {
|
|
@@ -170,10 +195,13 @@ export const runProgressServiceRenderer = (
|
|
|
170
195
|
});
|
|
171
196
|
|
|
172
197
|
return Effect.gen(function* () {
|
|
198
|
+
yield* rendererLatch.await;
|
|
199
|
+
rendererActive = true;
|
|
200
|
+
|
|
173
201
|
if (isTTY) {
|
|
174
202
|
process.stderr.write(HIDE_CURSOR);
|
|
175
203
|
|
|
176
|
-
if (
|
|
204
|
+
if (rendererConfig.disableUserInput && process.stdin.isTTY) {
|
|
177
205
|
const stdin = process.stdin;
|
|
178
206
|
const wasRaw = Boolean(stdin.isRaw);
|
|
179
207
|
stdin.resume();
|
|
@@ -214,11 +242,15 @@ export const runProgressServiceRenderer = (
|
|
|
214
242
|
}
|
|
215
243
|
|
|
216
244
|
tick += 1;
|
|
217
|
-
yield* Effect.sleep(
|
|
245
|
+
yield* Effect.sleep(Math.max(1, Math.floor(rendererConfig.renderIntervalMillis)));
|
|
218
246
|
}
|
|
219
247
|
}).pipe(
|
|
220
248
|
Effect.ensuring(
|
|
221
249
|
Effect.gen(function* () {
|
|
250
|
+
if (!rendererActive) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
222
254
|
yield* renderFrame("final");
|
|
223
255
|
|
|
224
256
|
if (isTTY) {
|
package/src/runtime.ts
CHANGED
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import { Effect, Exit, FiberRef, Option, Ref } from "effect";
|
|
1
|
+
import { Console, Effect, Exit, FiberRef, Option, Ref } from "effect";
|
|
2
|
+
import { mergeWith } from "es-toolkit/object";
|
|
2
3
|
import { formatWithOptions } from "node:util";
|
|
4
|
+
import type { PartialDeep } from "type-fest";
|
|
5
|
+
import { makeProgressConsole } from "./console";
|
|
3
6
|
import { runProgressServiceRenderer } from "./renderer";
|
|
4
7
|
import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
|
|
5
8
|
import {
|
|
9
|
+
decodeProgressBarConfigSync,
|
|
10
|
+
decodeRendererConfigSync,
|
|
6
11
|
defaultProgressBarConfig,
|
|
12
|
+
defaultRendererConfig,
|
|
7
13
|
DeterminateTaskUnits,
|
|
8
14
|
IndeterminateTaskUnits,
|
|
9
15
|
Progress,
|
|
10
16
|
ProgressBarConfig,
|
|
17
|
+
RendererConfig,
|
|
11
18
|
TaskId,
|
|
12
19
|
TaskSnapshot,
|
|
13
20
|
} from "./types";
|
|
@@ -15,6 +22,21 @@ import { inferTotal } from "./utils";
|
|
|
15
22
|
|
|
16
23
|
const DIRTY_DEBOUNCE_INTERVAL = "10 millis";
|
|
17
24
|
|
|
25
|
+
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
26
|
+
base: T,
|
|
27
|
+
override: PartialDeep<T> | undefined,
|
|
28
|
+
): T =>
|
|
29
|
+
mergeWith(
|
|
30
|
+
structuredClone(base),
|
|
31
|
+
(override ?? {}) as Record<PropertyKey, any>,
|
|
32
|
+
(_targetValue, sourceValue) => {
|
|
33
|
+
if (Array.isArray(sourceValue)) {
|
|
34
|
+
return sourceValue;
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
},
|
|
38
|
+
) as T;
|
|
39
|
+
|
|
18
40
|
const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
|
|
19
41
|
const currentUnits = snapshot.units;
|
|
20
42
|
const units = (() => {
|
|
@@ -54,13 +76,27 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
54
76
|
status: snapshot.status,
|
|
55
77
|
transient: options.transient ?? snapshot.transient,
|
|
56
78
|
units,
|
|
79
|
+
progressbar: snapshot.progressbar,
|
|
57
80
|
});
|
|
58
81
|
};
|
|
59
82
|
|
|
60
83
|
export const makeProgressService = Effect.gen(function* () {
|
|
61
|
-
const
|
|
62
|
-
const
|
|
63
|
-
|
|
84
|
+
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
85
|
+
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
86
|
+
|
|
87
|
+
const rendererConfig = decodeRendererConfigSync(
|
|
88
|
+
mergeConfig(
|
|
89
|
+
defaultRendererConfig,
|
|
90
|
+
Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
|
|
91
|
+
),
|
|
92
|
+
);
|
|
93
|
+
const progressBarConfig = decodeProgressBarConfigSync(
|
|
94
|
+
mergeConfig(
|
|
95
|
+
defaultProgressBarConfig,
|
|
96
|
+
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
97
|
+
),
|
|
98
|
+
);
|
|
99
|
+
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
64
100
|
|
|
65
101
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
66
102
|
const tasksRef = yield* Ref.make(new Map<TaskId, TaskSnapshot>());
|
|
@@ -68,17 +104,22 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
68
104
|
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
69
105
|
const dirtyRef = yield* Ref.make(true);
|
|
70
106
|
const dirtyScheduledRef = yield* Ref.make(false);
|
|
107
|
+
const rendererStartedRef = yield* Ref.make(false);
|
|
108
|
+
const rendererLatch = yield* Effect.makeLatch(false);
|
|
71
109
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
110
|
+
const scope = yield* Effect.scope;
|
|
72
111
|
|
|
73
|
-
yield* Effect.
|
|
112
|
+
yield* Effect.forkIn(
|
|
74
113
|
runProgressServiceRenderer(
|
|
75
114
|
tasksRef,
|
|
76
115
|
logsRef,
|
|
77
116
|
pendingLogsRef,
|
|
78
117
|
dirtyRef,
|
|
79
|
-
|
|
118
|
+
rendererConfig,
|
|
80
119
|
maxRetainedLogLines,
|
|
120
|
+
rendererLatch,
|
|
81
121
|
),
|
|
122
|
+
scope,
|
|
82
123
|
);
|
|
83
124
|
|
|
84
125
|
const markDirty = Effect.gen(function* () {
|
|
@@ -109,6 +150,19 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
109
150
|
options.total === undefined || options.total <= 0
|
|
110
151
|
? new IndeterminateTaskUnits({ spinnerFrame: 0 })
|
|
111
152
|
: new DeterminateTaskUnits({ completed: 0, total: Math.max(0, options.total) });
|
|
153
|
+
const tasks = yield* Ref.get(tasksRef);
|
|
154
|
+
const parentSnapshot =
|
|
155
|
+
Option.isSome(parentId) ? tasks.get(parentId.value) : undefined;
|
|
156
|
+
const inheritedProgressBarConfig = parentSnapshot?.progressbar ?? progressBarConfig;
|
|
157
|
+
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
158
|
+
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
159
|
+
);
|
|
160
|
+
const shouldOpenRenderer = yield* Ref.modify(rendererStartedRef, (started) =>
|
|
161
|
+
started ? [false, true] : [true, true],
|
|
162
|
+
);
|
|
163
|
+
if (shouldOpenRenderer) {
|
|
164
|
+
yield* rendererLatch.open;
|
|
165
|
+
}
|
|
112
166
|
|
|
113
167
|
const snapshot = new TaskSnapshot({
|
|
114
168
|
id: taskId,
|
|
@@ -117,6 +171,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
117
171
|
status: "running",
|
|
118
172
|
transient: options.transient ?? false,
|
|
119
173
|
units,
|
|
174
|
+
progressbar: resolvedProgressBarConfig,
|
|
120
175
|
});
|
|
121
176
|
|
|
122
177
|
yield* Ref.update(tasksRef, (tasks) => {
|
|
@@ -156,8 +211,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
156
211
|
total: snapshot.units.total,
|
|
157
212
|
})
|
|
158
213
|
: new IndeterminateTaskUnits({
|
|
159
|
-
spinnerFrame:
|
|
160
|
-
(snapshot.units.spinnerFrame + amount) % Math.max(1, config.spinnerFrames.length),
|
|
214
|
+
spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount),
|
|
161
215
|
});
|
|
162
216
|
|
|
163
217
|
next.set(
|
|
@@ -169,6 +223,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
169
223
|
status: snapshot.status,
|
|
170
224
|
transient: snapshot.transient,
|
|
171
225
|
units,
|
|
226
|
+
progressbar: snapshot.progressbar,
|
|
172
227
|
}),
|
|
173
228
|
);
|
|
174
229
|
|
|
@@ -203,6 +258,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
203
258
|
total: snapshot.units.total,
|
|
204
259
|
})
|
|
205
260
|
: snapshot.units,
|
|
261
|
+
progressbar: snapshot.progressbar,
|
|
206
262
|
}),
|
|
207
263
|
);
|
|
208
264
|
return next;
|
|
@@ -230,6 +286,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
230
286
|
status: "failed",
|
|
231
287
|
transient: snapshot.transient,
|
|
232
288
|
units: snapshot.units,
|
|
289
|
+
progressbar: snapshot.progressbar,
|
|
233
290
|
}),
|
|
234
291
|
);
|
|
235
292
|
return next;
|
|
@@ -241,9 +298,16 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
241
298
|
return;
|
|
242
299
|
}
|
|
243
300
|
|
|
301
|
+
const shouldOpenRenderer = yield* Ref.modify(rendererStartedRef, (started) =>
|
|
302
|
+
started ? [false, true] : [true, true],
|
|
303
|
+
);
|
|
304
|
+
if (shouldOpenRenderer) {
|
|
305
|
+
yield* rendererLatch.open;
|
|
306
|
+
}
|
|
307
|
+
|
|
244
308
|
const message = formatWithOptions(
|
|
245
309
|
{
|
|
246
|
-
colors:
|
|
310
|
+
colors: rendererConfig.isTTY,
|
|
247
311
|
depth: 6,
|
|
248
312
|
},
|
|
249
313
|
...args,
|
|
@@ -263,6 +327,12 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
263
327
|
yield* markDirty;
|
|
264
328
|
});
|
|
265
329
|
|
|
330
|
+
const withCapturedLogs: ProgressService["withCapturedLogs"] = (effect) =>
|
|
331
|
+
Effect.gen(function* () {
|
|
332
|
+
const outerConsole = yield* Console.consoleWith((console) => Effect.succeed(console));
|
|
333
|
+
return yield* Effect.withConsole(effect, makeProgressConsole(log, outerConsole));
|
|
334
|
+
});
|
|
335
|
+
|
|
266
336
|
const getTask = (taskId: TaskId) =>
|
|
267
337
|
Ref.get(tasksRef).pipe(Effect.map((tasks) => Option.fromNullable(tasks.get(taskId))));
|
|
268
338
|
|
|
@@ -302,6 +372,7 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
302
372
|
description: options.description,
|
|
303
373
|
total: options.total ?? inferTotal(iterable),
|
|
304
374
|
transient: options.transient,
|
|
375
|
+
progressbar: options.progressbar,
|
|
305
376
|
},
|
|
306
377
|
(taskId) => {
|
|
307
378
|
return Effect.forEach(iterable, (item, index) =>
|
|
@@ -310,16 +381,19 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
310
381
|
},
|
|
311
382
|
);
|
|
312
383
|
|
|
313
|
-
|
|
384
|
+
const service: ProgressService = {
|
|
314
385
|
addTask,
|
|
315
386
|
updateTask,
|
|
316
387
|
advanceTask,
|
|
317
388
|
completeTask,
|
|
318
389
|
failTask,
|
|
319
390
|
log,
|
|
391
|
+
withCapturedLogs,
|
|
320
392
|
getTask,
|
|
321
393
|
listTasks,
|
|
322
394
|
withTask,
|
|
323
395
|
trackIterable,
|
|
324
|
-
}
|
|
396
|
+
};
|
|
397
|
+
|
|
398
|
+
return Progress.of(service);
|
|
325
399
|
});
|
package/src/types.ts
CHANGED
|
@@ -1,39 +1,55 @@
|
|
|
1
1
|
import { Brand, Context, Effect, Option, Schema } from "effect";
|
|
2
|
+
import type { PartialDeep } from "type-fest";
|
|
3
|
+
import { defaultProgressBarColors, ProgressBarColorsSchema } from "./colors";
|
|
2
4
|
|
|
3
|
-
export const
|
|
4
|
-
export const DEFAULT_PROGRESS_BAR_WIDTH = 30;
|
|
5
|
-
|
|
6
|
-
export const ProgressBarConfigSchema = Schema.Struct({
|
|
5
|
+
export const RendererConfigSchema = Schema.Struct({
|
|
7
6
|
isTTY: Schema.Boolean,
|
|
8
7
|
disableUserInput: Schema.Boolean,
|
|
8
|
+
renderIntervalMillis: Schema.Number,
|
|
9
|
+
maxLogLines: Schema.optional(Schema.Number),
|
|
10
|
+
nonTtyUpdateStep: Schema.Number,
|
|
11
|
+
});
|
|
12
|
+
export type RendererConfigShape = typeof RendererConfigSchema.Type;
|
|
13
|
+
export const decodeRendererConfigSync = Schema.decodeUnknownSync(RendererConfigSchema);
|
|
14
|
+
|
|
15
|
+
export const ProgressBarConfigSchema = Schema.Struct({
|
|
9
16
|
spinnerFrames: Schema.NonEmptyArray(Schema.String),
|
|
10
17
|
barWidth: Schema.Number,
|
|
11
18
|
fillChar: Schema.String,
|
|
12
19
|
emptyChar: Schema.String,
|
|
13
20
|
leftBracket: Schema.String,
|
|
14
21
|
rightBracket: Schema.String,
|
|
15
|
-
|
|
16
|
-
nonTtyUpdateStep: Schema.Number,
|
|
22
|
+
colors: ProgressBarColorsSchema,
|
|
17
23
|
});
|
|
18
|
-
|
|
19
24
|
export type ProgressBarConfigShape = typeof ProgressBarConfigSchema.Type;
|
|
25
|
+
export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarConfigSchema);
|
|
20
26
|
|
|
21
|
-
export const
|
|
27
|
+
export const defaultRendererConfig: RendererConfigShape = {
|
|
22
28
|
isTTY: Boolean(process.stderr.isTTY),
|
|
23
29
|
disableUserInput: true,
|
|
24
|
-
|
|
25
|
-
|
|
30
|
+
renderIntervalMillis: 50, // 20 FPS
|
|
31
|
+
maxLogLines: 0,
|
|
32
|
+
nonTtyUpdateStep: 5,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const defaultProgressBarConfig: ProgressBarConfigShape = {
|
|
36
|
+
spinnerFrames: ["-", "\\", "|", "/"],
|
|
37
|
+
barWidth: 30,
|
|
26
38
|
fillChar: "━",
|
|
27
39
|
emptyChar: "─",
|
|
28
40
|
leftBracket: "",
|
|
29
41
|
rightBracket: "",
|
|
30
|
-
|
|
31
|
-
nonTtyUpdateStep: 5,
|
|
42
|
+
colors: defaultProgressBarColors,
|
|
32
43
|
};
|
|
33
44
|
|
|
45
|
+
export class RendererConfig extends Context.Tag("stromseng.dev/RendererConfig")<
|
|
46
|
+
RendererConfig,
|
|
47
|
+
PartialDeep<RendererConfigShape>
|
|
48
|
+
>() {}
|
|
49
|
+
|
|
34
50
|
export class ProgressBarConfig extends Context.Tag("stromseng.dev/ProgressBarConfig")<
|
|
35
51
|
ProgressBarConfig,
|
|
36
|
-
ProgressBarConfigShape
|
|
52
|
+
PartialDeep<ProgressBarConfigShape>
|
|
37
53
|
>() {}
|
|
38
54
|
|
|
39
55
|
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
@@ -50,6 +66,7 @@ export interface AddTaskOptions {
|
|
|
50
66
|
readonly total?: number;
|
|
51
67
|
readonly transient?: boolean;
|
|
52
68
|
readonly parentId?: TaskId;
|
|
69
|
+
readonly progressbar?: PartialDeep<ProgressBarConfigShape>;
|
|
53
70
|
}
|
|
54
71
|
|
|
55
72
|
export interface UpdateTaskOptions {
|
|
@@ -87,6 +104,7 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
|
|
|
87
104
|
status: TaskStatusSchema,
|
|
88
105
|
transient: Schema.Boolean,
|
|
89
106
|
units: TaskUnitsSchema,
|
|
107
|
+
progressbar: ProgressBarConfigSchema,
|
|
90
108
|
}) {}
|
|
91
109
|
|
|
92
110
|
export interface ProgressService {
|
|
@@ -96,6 +114,7 @@ export interface ProgressService {
|
|
|
96
114
|
readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
97
115
|
readonly failTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
98
116
|
readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
|
|
117
|
+
readonly withCapturedLogs: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
99
118
|
readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
|
|
100
119
|
readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
|
|
101
120
|
readonly withTask: <A, E, R>(
|