effective-progress 0.1.3 → 0.2.1
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 +40 -14
- package/package.json +2 -2
- package/src/api.ts +111 -82
- package/src/colors.ts +4 -54
- package/src/index.ts +2 -1
- package/src/renderer.ts +100 -68
- package/src/runtime.ts +76 -77
- package/src/terminal.ts +61 -0
- package/src/types.ts +11 -9
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/effective-progress)
|
|
4
4
|
|
|
5
5
|
> [!WARNING]
|
|
6
|
-
> Pre-`1.0.0`, breaking changes may happen in any release. SemVer guarantees will begin at `1.0.0`.
|
|
6
|
+
> Pre-`1.0.0`, breaking changes may happen in any release. SemVer guarantees will begin at `1.0.0`.
|
|
7
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
8
|
>
|
|
9
9
|
> Please open an issue or reach out if you have any questions or want to contribute!
|
|
@@ -82,6 +82,7 @@ Effect.runPromise(program);
|
|
|
82
82
|
- `examples/simpleExample.ts` - low-boilerplate real-world flow
|
|
83
83
|
- `examples/advancedExample.ts` - full API usage with custom config and manual task control
|
|
84
84
|
- `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
|
|
85
|
+
- `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
|
|
85
86
|
|
|
86
87
|
## Log retention
|
|
87
88
|
|
|
@@ -119,6 +120,7 @@ Task-level `progressbar` config is optional and inherits from its parent task (o
|
|
|
119
120
|
```ts
|
|
120
121
|
yield *
|
|
121
122
|
progress.withTask(
|
|
123
|
+
Effect.sleep("1 second"),
|
|
122
124
|
{
|
|
123
125
|
description: "Worker pipeline",
|
|
124
126
|
progressbar: {
|
|
@@ -129,26 +131,50 @@ yield *
|
|
|
129
131
|
},
|
|
130
132
|
},
|
|
131
133
|
},
|
|
132
|
-
() => Effect.sleep("1 second"),
|
|
133
134
|
);
|
|
134
135
|
```
|
|
135
136
|
|
|
136
|
-
|
|
137
|
+
## Terminal service and mocking
|
|
138
|
+
|
|
139
|
+
`effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
|
|
140
|
+
|
|
141
|
+
- `isTTY`
|
|
142
|
+
- `stderrRows`
|
|
143
|
+
- `stderrColumns`
|
|
144
|
+
- `writeStderr(text)`
|
|
145
|
+
- `withRawInputCapture(effect)`
|
|
146
|
+
|
|
147
|
+
You can provide a mock if you want to alter the behavior of terminal detection or if you want to capture the output for testing:
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { Effect } from "effect";
|
|
151
|
+
import * as Progress from "effective-progress";
|
|
152
|
+
|
|
153
|
+
const mockTerminal: Progress.ProgressTerminalService = {
|
|
154
|
+
isTTY: Effect.succeed(true),
|
|
155
|
+
stderrRows: Effect.succeed(40),
|
|
156
|
+
stderrColumns: Effect.succeed(120),
|
|
157
|
+
writeStderr: (_text) => Effect.void,
|
|
158
|
+
withRawInputCapture: (effect) => effect,
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const program = Progress.withTask(Effect.sleep("100 millis"), { description: "work" }).pipe(
|
|
162
|
+
Effect.provideService(Progress.ProgressTerminal, mockTerminal),
|
|
163
|
+
);
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## Manual task control
|
|
167
|
+
|
|
168
|
+
For manual usage, `withTask` captures logs implicitly and provides the current `Task` context:
|
|
137
169
|
|
|
138
170
|
```ts
|
|
139
|
-
const program = Progress.
|
|
171
|
+
const program = Progress.withTask(
|
|
140
172
|
Effect.gen(function* () {
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
yield*
|
|
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
|
-
);
|
|
173
|
+
const currentTask = yield* Progress.Task;
|
|
174
|
+
yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
|
|
175
|
+
yield* Effect.sleep("1 second");
|
|
151
176
|
}),
|
|
177
|
+
{ description: "Manual task" },
|
|
152
178
|
);
|
|
153
179
|
```
|
|
154
180
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effective-progress",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"dependencies": {
|
|
33
33
|
"chalk": "^5.6.2",
|
|
34
|
-
"effect": "^3.19.
|
|
34
|
+
"effect": "^3.19.17",
|
|
35
35
|
"es-toolkit": "^1.44.0",
|
|
36
36
|
"type-fest": "^5.4.4"
|
|
37
37
|
},
|
package/src/api.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import { Effect
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
2
3
|
import type { Concurrency } from "effect/Types";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import type { TrackOptions } from "./types";
|
|
4
|
+
import { Progress, provideProgressService } from "./runtime";
|
|
5
|
+
import { Task } from "./types";
|
|
6
|
+
import type { AddTaskOptions, TrackOptions } from "./types";
|
|
6
7
|
import { inferTotal } from "./utils";
|
|
7
8
|
|
|
8
9
|
export interface EffectExecutionOptions {
|
|
@@ -22,7 +23,7 @@ export type AllReturn<
|
|
|
22
23
|
O extends EffectAllExecutionOptions,
|
|
23
24
|
> =
|
|
24
25
|
[Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>] extends
|
|
25
|
-
[Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress>>
|
|
26
|
+
[Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress | Task>>
|
|
26
27
|
: never;
|
|
27
28
|
|
|
28
29
|
export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
@@ -31,85 +32,113 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
|
31
32
|
|
|
32
33
|
export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
33
34
|
|
|
34
|
-
export const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return yield* Effect.provideService(effect, Progress, service);
|
|
45
|
-
}),
|
|
46
|
-
);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
export const all = <
|
|
50
|
-
const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>,
|
|
51
|
-
O extends EffectAllExecutionOptions,
|
|
52
|
-
>(
|
|
53
|
-
effects: Arg,
|
|
54
|
-
options: Omit<TrackOptions, "total"> & O,
|
|
55
|
-
): AllReturn<Arg, O> =>
|
|
56
|
-
provide(
|
|
35
|
+
export const withTask: {
|
|
36
|
+
<A, E, R>(
|
|
37
|
+
effect: Effect.Effect<A, E, R>,
|
|
38
|
+
options: AddTaskOptions,
|
|
39
|
+
): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
40
|
+
<A, E, R>(
|
|
41
|
+
options: AddTaskOptions,
|
|
42
|
+
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
43
|
+
} = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
44
|
+
provideProgressService(
|
|
57
45
|
Effect.gen(function* () {
|
|
58
46
|
const progress = yield* Progress;
|
|
59
|
-
return yield* progress.withTask(
|
|
60
|
-
{
|
|
61
|
-
description: options.description,
|
|
62
|
-
total: effects.length,
|
|
63
|
-
transient: options.transient,
|
|
64
|
-
progressbar: options.progressbar,
|
|
65
|
-
},
|
|
66
|
-
(taskId) =>
|
|
67
|
-
Effect.all(
|
|
68
|
-
effects.map((effect) =>
|
|
69
|
-
Effect.tap(progress.withCapturedLogs(effect), () => progress.advanceTask(taskId, 1)),
|
|
70
|
-
),
|
|
71
|
-
{
|
|
72
|
-
concurrency: options.concurrency,
|
|
73
|
-
batching: options.batching,
|
|
74
|
-
discard: options.discard,
|
|
75
|
-
mode: options.mode,
|
|
76
|
-
concurrentFinalizers: options.concurrentFinalizers,
|
|
77
|
-
},
|
|
78
|
-
),
|
|
79
|
-
);
|
|
47
|
+
return yield* progress.withTask(effect, options);
|
|
80
48
|
}),
|
|
81
|
-
) as
|
|
49
|
+
) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
|
|
50
|
+
);
|
|
82
51
|
|
|
83
|
-
export const
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
):
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
52
|
+
export const all: {
|
|
53
|
+
<const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
|
|
54
|
+
effects: Arg,
|
|
55
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
56
|
+
): AllReturn<Arg, O>;
|
|
57
|
+
<O extends EffectAllExecutionOptions>(
|
|
58
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
59
|
+
): <const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>>(effects: Arg) => AllReturn<
|
|
60
|
+
Arg,
|
|
61
|
+
O
|
|
62
|
+
>;
|
|
63
|
+
} = dual(
|
|
64
|
+
2,
|
|
65
|
+
<const Arg extends ReadonlyArray<Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions>(
|
|
66
|
+
effects: Arg,
|
|
67
|
+
options: Omit<TrackOptions, "total"> & O,
|
|
68
|
+
) =>
|
|
69
|
+
provideProgressService(
|
|
70
|
+
Effect.gen(function* () {
|
|
71
|
+
const progress = yield* Progress;
|
|
72
|
+
return yield* progress.withTask(
|
|
73
|
+
Effect.gen(function* () {
|
|
74
|
+
const taskId = yield* Task;
|
|
75
|
+
return yield* Effect.all(
|
|
76
|
+
effects.map((effect) => Effect.tap(effect, () => progress.advanceTask(taskId, 1))),
|
|
77
|
+
{
|
|
78
|
+
concurrency: options.concurrency,
|
|
79
|
+
batching: options.batching,
|
|
80
|
+
discard: options.discard,
|
|
81
|
+
mode: options.mode,
|
|
82
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
83
|
+
},
|
|
84
|
+
);
|
|
85
|
+
}),
|
|
86
|
+
{
|
|
87
|
+
description: options.description,
|
|
88
|
+
total: effects.length,
|
|
89
|
+
transient: options.transient,
|
|
90
|
+
progressbar: options.progressbar,
|
|
91
|
+
},
|
|
92
|
+
);
|
|
93
|
+
}),
|
|
94
|
+
) as AllReturn<Arg, O>,
|
|
95
|
+
);
|
|
91
96
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
97
|
+
export const forEach: {
|
|
98
|
+
<A, B, E, R>(
|
|
99
|
+
iterable: Iterable<A>,
|
|
100
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
101
|
+
options: ForEachOptions,
|
|
102
|
+
): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
103
|
+
<A, B, E, R>(
|
|
104
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
105
|
+
options: ForEachOptions,
|
|
106
|
+
): (
|
|
107
|
+
iterable: Iterable<A>,
|
|
108
|
+
) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
109
|
+
} = dual(
|
|
110
|
+
3,
|
|
111
|
+
<A, B, E, R>(
|
|
112
|
+
iterable: Iterable<A>,
|
|
113
|
+
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
114
|
+
options: ForEachOptions,
|
|
115
|
+
) =>
|
|
116
|
+
provideProgressService(
|
|
117
|
+
Effect.gen(function* () {
|
|
118
|
+
const progress = yield* Progress;
|
|
119
|
+
|
|
120
|
+
return yield* progress.withTask(
|
|
121
|
+
Effect.gen(function* () {
|
|
122
|
+
const taskId = yield* Task;
|
|
123
|
+
return yield* Effect.forEach(
|
|
124
|
+
iterable,
|
|
125
|
+
(item, index) =>
|
|
126
|
+
Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
|
|
127
|
+
{
|
|
128
|
+
concurrency: options.concurrency,
|
|
129
|
+
batching: options.batching,
|
|
130
|
+
discard: options.discard,
|
|
131
|
+
concurrentFinalizers: options.concurrentFinalizers,
|
|
132
|
+
},
|
|
133
|
+
);
|
|
134
|
+
}),
|
|
135
|
+
{
|
|
136
|
+
description: options.description,
|
|
137
|
+
total: options.total ?? inferTotal(iterable),
|
|
138
|
+
transient: options.transient,
|
|
139
|
+
progressbar: options.progressbar,
|
|
140
|
+
},
|
|
141
|
+
);
|
|
142
|
+
}),
|
|
143
|
+
) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>,
|
|
144
|
+
);
|
package/src/colors.ts
CHANGED
|
@@ -103,61 +103,11 @@ export const defaultProgressBarColors: ProgressBarColors = {
|
|
|
103
103
|
failed: { kind: "named", value: "red" },
|
|
104
104
|
};
|
|
105
105
|
|
|
106
|
-
const applyModifier = (instance: ChalkInstance, modifier: StyleModifier): ChalkInstance =>
|
|
107
|
-
|
|
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
|
-
};
|
|
106
|
+
const applyModifier = (instance: ChalkInstance, modifier: StyleModifier): ChalkInstance =>
|
|
107
|
+
instance[modifier] as ChalkInstance;
|
|
124
108
|
|
|
125
|
-
const applyNamedColor = (instance: ChalkInstance, color: NamedColor): ChalkInstance =>
|
|
126
|
-
|
|
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
|
-
};
|
|
109
|
+
const applyNamedColor = (instance: ChalkInstance, color: NamedColor): ChalkInstance =>
|
|
110
|
+
instance[color] as ChalkInstance;
|
|
161
111
|
|
|
162
112
|
const resolveBaseStyle = (style: ColorStyle): ChalkInstance => {
|
|
163
113
|
switch (style.kind) {
|
package/src/index.ts
CHANGED
package/src/renderer.ts
CHANGED
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
compileProgressBarColors,
|
|
5
5
|
ProgressBarColorsSchema,
|
|
6
6
|
} from "./colors";
|
|
7
|
+
import type { ProgressTerminalService } from "./terminal";
|
|
7
8
|
import type { ProgressBarConfigShape, RendererConfigShape } from "./types";
|
|
8
9
|
import { DeterminateTaskUnits, TaskId, TaskSnapshot } from "./types";
|
|
9
10
|
|
|
@@ -84,19 +85,18 @@ export const runProgressServiceRenderer = (
|
|
|
84
85
|
logsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
85
86
|
pendingLogsRef: Ref.Ref<ReadonlyArray<string>>,
|
|
86
87
|
dirtyRef: Ref.Ref<boolean>,
|
|
88
|
+
terminal: ProgressTerminalService,
|
|
89
|
+
isTTY: boolean,
|
|
87
90
|
rendererConfig: RendererConfigShape,
|
|
88
91
|
maxRetainedLogLines: number,
|
|
89
|
-
rendererLatch: Effect.Latch,
|
|
90
92
|
) => {
|
|
91
|
-
const isTTY = rendererConfig.isTTY;
|
|
92
93
|
const retainLogHistory = maxRetainedLogLines > 0;
|
|
93
94
|
const colorCache = new Map<string, CompiledProgressBarColors>();
|
|
94
95
|
let previousLineCount = 0;
|
|
95
|
-
let previousTaskLineCount = 0;
|
|
96
96
|
let nonTTYTaskSignatureById = new Map<number, string>();
|
|
97
97
|
let tick = 0;
|
|
98
98
|
let rendererActive = false;
|
|
99
|
-
let
|
|
99
|
+
let sessionActive = false;
|
|
100
100
|
|
|
101
101
|
const getCompiledColors = (progressbar: ProgressBarConfigShape): CompiledProgressBarColors => {
|
|
102
102
|
const key = encodeProgressBarColorsKey(progressbar.colors);
|
|
@@ -110,17 +110,47 @@ export const runProgressServiceRenderer = (
|
|
|
110
110
|
return compiled;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
-
const
|
|
114
|
-
|
|
113
|
+
const clipTTYFrameLines = (lines: ReadonlyArray<string>) =>
|
|
114
|
+
Effect.gen(function* () {
|
|
115
|
+
const terminalRows = yield* terminal.stderrRows;
|
|
116
|
+
if (terminalRows === undefined) {
|
|
117
|
+
return lines;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const visibleLineLimit = Math.max(1, terminalRows - 1);
|
|
121
|
+
if (lines.length <= visibleLineLimit) {
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (visibleLineLimit === 1) {
|
|
126
|
+
return [`... ${lines.length} lines hidden`];
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const hiddenLineCount = lines.length - visibleLineLimit + 1;
|
|
130
|
+
return [
|
|
131
|
+
`... ${hiddenLineCount} lines hidden (showing latest lines)`,
|
|
132
|
+
...lines.slice(lines.length - (visibleLineLimit - 1)),
|
|
133
|
+
];
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const startTTYSession = Effect.gen(function* () {
|
|
137
|
+
if (!isTTY || sessionActive) {
|
|
115
138
|
return;
|
|
116
139
|
}
|
|
117
140
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
141
|
+
yield* terminal.writeStderr(HIDE_CURSOR);
|
|
142
|
+
sessionActive = true;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const stopTTYSession = Effect.gen(function* () {
|
|
146
|
+
if (!isTTY || !sessionActive) {
|
|
147
|
+
return;
|
|
121
148
|
}
|
|
122
|
-
|
|
123
|
-
|
|
149
|
+
|
|
150
|
+
yield* terminal.writeStderr("\n" + SHOW_CURSOR);
|
|
151
|
+
previousLineCount = 0;
|
|
152
|
+
sessionActive = false;
|
|
153
|
+
});
|
|
124
154
|
|
|
125
155
|
const renderNonTTYTaskUpdates = (
|
|
126
156
|
ordered: ReadonlyArray<{ snapshot: TaskSnapshot; depth: number }>,
|
|
@@ -145,11 +175,13 @@ export const runProgressServiceRenderer = (
|
|
|
145
175
|
}
|
|
146
176
|
}
|
|
147
177
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
178
|
+
return Effect.gen(function* () {
|
|
179
|
+
if (changedTaskLines.length > 0) {
|
|
180
|
+
yield* terminal.writeStderr(changedTaskLines.join("\n") + "\n");
|
|
181
|
+
}
|
|
151
182
|
|
|
152
|
-
|
|
183
|
+
nonTTYTaskSignatureById = nextTaskSignatureById;
|
|
184
|
+
});
|
|
153
185
|
};
|
|
154
186
|
|
|
155
187
|
const renderFrame = (mode: "tick" | "final") =>
|
|
@@ -166,78 +198,69 @@ export const runProgressServiceRenderer = (
|
|
|
166
198
|
});
|
|
167
199
|
|
|
168
200
|
if (isTTY) {
|
|
201
|
+
let frame = "";
|
|
202
|
+
|
|
203
|
+
// 1. Cursor reset — move up and clear previous frame lines
|
|
204
|
+
if (previousLineCount > 0) {
|
|
205
|
+
frame += "\r" + CLEAR_LINE;
|
|
206
|
+
for (let i = 1; i < previousLineCount; i++) {
|
|
207
|
+
frame += MOVE_UP_ONE + CLEAR_LINE;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
169
211
|
if (retainLogHistory) {
|
|
170
212
|
const historyLogs = yield* Ref.get(logsRef);
|
|
171
|
-
const lines = [...historyLogs, ...taskLines];
|
|
172
|
-
clearTTYLines(previousLineCount);
|
|
213
|
+
const lines = yield* clipTTYFrameLines([...historyLogs, ...taskLines]);
|
|
173
214
|
if (lines.length > 0) {
|
|
174
|
-
|
|
215
|
+
frame += lines.join("\n");
|
|
175
216
|
}
|
|
176
217
|
previousLineCount = lines.length;
|
|
177
|
-
|
|
218
|
+
} else {
|
|
219
|
+
// 2. Logs (scroll above the task block)
|
|
220
|
+
if (drainedLogs.length > 0) {
|
|
221
|
+
frame += drainedLogs.join("\n") + "\n";
|
|
222
|
+
}
|
|
223
|
+
// 3. Task lines
|
|
224
|
+
if (taskLines.length > 0) {
|
|
225
|
+
frame += taskLines.join("\n");
|
|
226
|
+
}
|
|
227
|
+
previousLineCount = taskLines.length;
|
|
178
228
|
}
|
|
179
229
|
|
|
180
|
-
|
|
181
|
-
if (
|
|
182
|
-
|
|
230
|
+
// 4. Single atomic write
|
|
231
|
+
if (frame) {
|
|
232
|
+
yield* terminal.writeStderr(frame);
|
|
183
233
|
}
|
|
184
|
-
if (taskLines.length > 0) {
|
|
185
|
-
process.stderr.write(taskLines.join("\n"));
|
|
186
|
-
}
|
|
187
|
-
previousTaskLineCount = taskLines.length;
|
|
188
234
|
return;
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
if (drainedLogs.length > 0) {
|
|
192
|
-
|
|
238
|
+
yield* terminal.writeStderr(drainedLogs.join("\n") + "\n");
|
|
193
239
|
}
|
|
194
|
-
renderNonTTYTaskUpdates(ordered, taskLines);
|
|
240
|
+
yield* renderNonTTYTaskUpdates(ordered, taskLines);
|
|
195
241
|
});
|
|
196
242
|
|
|
197
|
-
|
|
198
|
-
yield* rendererLatch.await;
|
|
243
|
+
const renderLoop = Effect.gen(function* () {
|
|
199
244
|
rendererActive = true;
|
|
200
|
-
|
|
201
245
|
if (isTTY) {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
if (rendererConfig.disableUserInput && process.stdin.isTTY) {
|
|
205
|
-
const stdin = process.stdin;
|
|
206
|
-
const wasRaw = Boolean(stdin.isRaw);
|
|
207
|
-
stdin.resume();
|
|
208
|
-
stdin.setRawMode?.(true);
|
|
209
|
-
|
|
210
|
-
const onData = (chunk: Buffer) => {
|
|
211
|
-
if (chunk.length === 1 && chunk[0] === 3) {
|
|
212
|
-
process.kill(process.pid, "SIGINT");
|
|
213
|
-
}
|
|
214
|
-
};
|
|
215
|
-
|
|
216
|
-
stdin.on("data", onData);
|
|
217
|
-
|
|
218
|
-
teardownInput = () => {
|
|
219
|
-
try {
|
|
220
|
-
stdin.off("data", onData);
|
|
221
|
-
stdin.setRawMode?.(wasRaw);
|
|
222
|
-
stdin.pause();
|
|
223
|
-
} catch {
|
|
224
|
-
// Best effort terminal restoration.
|
|
225
|
-
}
|
|
226
|
-
};
|
|
227
|
-
}
|
|
246
|
+
yield* startTTYSession;
|
|
228
247
|
}
|
|
229
248
|
|
|
230
249
|
while (true) {
|
|
231
250
|
const dirty = yield* Ref.getAndSet(dirtyRef, false);
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
),
|
|
251
|
+
const tasks = Array.from((yield* Ref.get(tasksRef)).values()).filter(
|
|
252
|
+
(task) => !(task.transient && task.status !== "running"),
|
|
253
|
+
);
|
|
254
|
+
const hasActiveSpinners = tasks.some(
|
|
255
|
+
(task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits",
|
|
238
256
|
);
|
|
257
|
+
const hasPendingLogs = (yield* Ref.get(pendingLogsRef)).length > 0;
|
|
239
258
|
|
|
240
|
-
if (
|
|
259
|
+
if (isTTY) {
|
|
260
|
+
if (dirty || hasActiveSpinners || hasPendingLogs) {
|
|
261
|
+
yield* renderFrame("tick");
|
|
262
|
+
}
|
|
263
|
+
} else if (dirty || hasActiveSpinners) {
|
|
241
264
|
yield* renderFrame("tick");
|
|
242
265
|
}
|
|
243
266
|
|
|
@@ -251,13 +274,22 @@ export const runProgressServiceRenderer = (
|
|
|
251
274
|
return;
|
|
252
275
|
}
|
|
253
276
|
|
|
254
|
-
yield* renderFrame("final");
|
|
255
|
-
|
|
256
277
|
if (isTTY) {
|
|
257
|
-
|
|
258
|
-
|
|
278
|
+
if (sessionActive) {
|
|
279
|
+
yield* renderFrame("final");
|
|
280
|
+
yield* stopTTYSession;
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
259
283
|
}
|
|
284
|
+
|
|
285
|
+
yield* renderFrame("final");
|
|
260
286
|
}),
|
|
261
287
|
),
|
|
262
288
|
);
|
|
289
|
+
|
|
290
|
+
if (isTTY && rendererConfig.disableUserInput) {
|
|
291
|
+
return terminal.withRawInputCapture(renderLoop);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return renderLoop;
|
|
263
295
|
};
|
package/src/runtime.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
|
|
2
|
+
import { dual } from "effect/Function";
|
|
2
3
|
import { mergeWith } from "es-toolkit/object";
|
|
3
4
|
import { formatWithOptions } from "node:util";
|
|
4
5
|
import type { PartialDeep } from "type-fest";
|
|
5
6
|
import { makeProgressConsole } from "./console";
|
|
6
7
|
import { runProgressServiceRenderer } from "./renderer";
|
|
8
|
+
import { ProgressTerminal } from "./terminal";
|
|
7
9
|
import type { AddTaskOptions, ProgressService, UpdateTaskOptions } from "./types";
|
|
8
10
|
import {
|
|
9
11
|
decodeProgressBarConfigSync,
|
|
@@ -12,16 +14,14 @@ import {
|
|
|
12
14
|
defaultRendererConfig,
|
|
13
15
|
DeterminateTaskUnits,
|
|
14
16
|
IndeterminateTaskUnits,
|
|
15
|
-
Progress,
|
|
16
17
|
ProgressBarConfig,
|
|
17
18
|
RendererConfig,
|
|
19
|
+
Task,
|
|
18
20
|
TaskId,
|
|
19
21
|
TaskSnapshot,
|
|
20
22
|
} from "./types";
|
|
21
23
|
import { inferTotal } from "./utils";
|
|
22
24
|
|
|
23
|
-
const DIRTY_DEBOUNCE_INTERVAL = "10 millis";
|
|
24
|
-
|
|
25
25
|
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
26
26
|
base: T,
|
|
27
27
|
override: PartialDeep<T> | undefined,
|
|
@@ -80,7 +80,7 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
80
80
|
});
|
|
81
81
|
};
|
|
82
82
|
|
|
83
|
-
|
|
83
|
+
const makeProgressService = Effect.gen(function* () {
|
|
84
84
|
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
85
85
|
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
86
86
|
|
|
@@ -96,6 +96,8 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
96
96
|
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
97
97
|
),
|
|
98
98
|
);
|
|
99
|
+
const terminal = yield* ProgressTerminal;
|
|
100
|
+
const isTTY = yield* terminal.isTTY;
|
|
99
101
|
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
100
102
|
|
|
101
103
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
@@ -103,9 +105,6 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
103
105
|
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
104
106
|
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
105
107
|
const dirtyRef = yield* Ref.make(true);
|
|
106
|
-
const dirtyScheduledRef = yield* Ref.make(false);
|
|
107
|
-
const rendererStartedRef = yield* Ref.make(false);
|
|
108
|
-
const rendererLatch = yield* Effect.makeLatch(false);
|
|
109
108
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
110
109
|
const scope = yield* Effect.scope;
|
|
111
110
|
|
|
@@ -115,29 +114,15 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
115
114
|
logsRef,
|
|
116
115
|
pendingLogsRef,
|
|
117
116
|
dirtyRef,
|
|
117
|
+
terminal,
|
|
118
|
+
isTTY,
|
|
118
119
|
rendererConfig,
|
|
119
120
|
maxRetainedLogLines,
|
|
120
|
-
rendererLatch,
|
|
121
121
|
),
|
|
122
122
|
scope,
|
|
123
123
|
);
|
|
124
124
|
|
|
125
|
-
const markDirty =
|
|
126
|
-
const shouldSchedule = yield* Ref.modify(dirtyScheduledRef, (scheduled) =>
|
|
127
|
-
scheduled ? [false, true] : [true, true],
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
if (!shouldSchedule) {
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
yield* Effect.forkDaemon(
|
|
135
|
-
Effect.sleep(DIRTY_DEBOUNCE_INTERVAL).pipe(
|
|
136
|
-
Effect.zipRight(Ref.set(dirtyRef, true)),
|
|
137
|
-
Effect.ensuring(Ref.set(dirtyScheduledRef, false)),
|
|
138
|
-
),
|
|
139
|
-
);
|
|
140
|
-
});
|
|
125
|
+
const markDirty = Ref.set(dirtyRef, true);
|
|
141
126
|
|
|
142
127
|
const addTask = (options: AddTaskOptions) =>
|
|
143
128
|
Effect.gen(function* () {
|
|
@@ -151,18 +136,11 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
151
136
|
? new IndeterminateTaskUnits({ spinnerFrame: 0 })
|
|
152
137
|
: new DeterminateTaskUnits({ completed: 0, total: Math.max(0, options.total) });
|
|
153
138
|
const tasks = yield* Ref.get(tasksRef);
|
|
154
|
-
const parentSnapshot =
|
|
155
|
-
Option.isSome(parentId) ? tasks.get(parentId.value) : undefined;
|
|
139
|
+
const parentSnapshot = Option.isSome(parentId) ? tasks.get(parentId.value) : undefined;
|
|
156
140
|
const inheritedProgressBarConfig = parentSnapshot?.progressbar ?? progressBarConfig;
|
|
157
141
|
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
158
142
|
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
159
143
|
);
|
|
160
|
-
const shouldOpenRenderer = yield* Ref.modify(rendererStartedRef, (started) =>
|
|
161
|
-
started ? [false, true] : [true, true],
|
|
162
|
-
);
|
|
163
|
-
if (shouldOpenRenderer) {
|
|
164
|
-
yield* rendererLatch.open;
|
|
165
|
-
}
|
|
166
144
|
|
|
167
145
|
const snapshot = new TaskSnapshot({
|
|
168
146
|
id: taskId,
|
|
@@ -292,22 +270,16 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
292
270
|
return next;
|
|
293
271
|
}).pipe(Effect.zipRight(markDirty));
|
|
294
272
|
|
|
295
|
-
const
|
|
273
|
+
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
296
274
|
Effect.gen(function* () {
|
|
297
275
|
if (args.length === 0) {
|
|
298
276
|
return;
|
|
299
277
|
}
|
|
300
278
|
|
|
301
|
-
|
|
302
|
-
started ? [false, true] : [true, true],
|
|
303
|
-
);
|
|
304
|
-
if (shouldOpenRenderer) {
|
|
305
|
-
yield* rendererLatch.open;
|
|
306
|
-
}
|
|
307
|
-
|
|
279
|
+
// TODO: Might wanna replace this or make it configurable. Look for other options.
|
|
308
280
|
const message = formatWithOptions(
|
|
309
281
|
{
|
|
310
|
-
colors:
|
|
282
|
+
colors: isTTY,
|
|
311
283
|
depth: 6,
|
|
312
284
|
},
|
|
313
285
|
...args,
|
|
@@ -327,58 +299,66 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
327
299
|
yield* markDirty;
|
|
328
300
|
});
|
|
329
301
|
|
|
330
|
-
const
|
|
331
|
-
Effect.gen(function* () {
|
|
332
|
-
const outerConsole = yield* Console.consoleWith((console) => Effect.succeed(console));
|
|
333
|
-
return yield* Effect.withConsole(effect, makeProgressConsole(log, outerConsole));
|
|
334
|
-
});
|
|
302
|
+
const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
|
|
335
303
|
|
|
336
304
|
const getTask = (taskId: TaskId) =>
|
|
337
305
|
Ref.get(tasksRef).pipe(Effect.map((tasks) => Option.fromNullable(tasks.get(taskId))));
|
|
338
306
|
|
|
339
307
|
const listTasks = Ref.get(tasksRef).pipe(Effect.map((tasks) => Array.from(tasks.values())));
|
|
340
308
|
|
|
341
|
-
const withTask: ProgressService["withTask"] = (
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
309
|
+
const withTask: ProgressService["withTask"] = dual(
|
|
310
|
+
2,
|
|
311
|
+
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
312
|
+
Effect.gen(function* () {
|
|
313
|
+
const outerConsole = yield* Effect.console;
|
|
314
|
+
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
315
|
+
const resolvedParentId =
|
|
316
|
+
options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
|
|
317
|
+
|
|
318
|
+
const taskId = yield* addTask({
|
|
319
|
+
...options,
|
|
320
|
+
parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : undefined,
|
|
321
|
+
transient: options.transient ?? Option.isSome(resolvedParentId),
|
|
322
|
+
});
|
|
352
323
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
324
|
+
const exit = yield* Effect.exit(
|
|
325
|
+
Effect.locally(
|
|
326
|
+
Effect.withConsole(
|
|
327
|
+
Effect.provideService(effect, Task, taskId),
|
|
328
|
+
makeProgressConsole(log, outerConsole),
|
|
329
|
+
),
|
|
330
|
+
currentParentRef,
|
|
331
|
+
Option.some(taskId),
|
|
332
|
+
),
|
|
333
|
+
);
|
|
356
334
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
335
|
+
if (Exit.isSuccess(exit)) {
|
|
336
|
+
yield* completeTask(taskId);
|
|
337
|
+
} else {
|
|
338
|
+
yield* failTask(taskId);
|
|
339
|
+
}
|
|
362
340
|
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
341
|
+
return yield* Exit.match(exit, {
|
|
342
|
+
onFailure: Effect.failCause,
|
|
343
|
+
onSuccess: Effect.succeed,
|
|
344
|
+
});
|
|
345
|
+
}),
|
|
346
|
+
);
|
|
368
347
|
|
|
369
348
|
const trackIterable: ProgressService["trackIterable"] = (iterable, options, f) =>
|
|
370
349
|
withTask(
|
|
350
|
+
Effect.gen(function* () {
|
|
351
|
+
const taskId = yield* Task;
|
|
352
|
+
return yield* Effect.forEach(iterable, (item, index) =>
|
|
353
|
+
Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
|
|
354
|
+
);
|
|
355
|
+
}),
|
|
371
356
|
{
|
|
372
357
|
description: options.description,
|
|
373
358
|
total: options.total ?? inferTotal(iterable),
|
|
374
359
|
transient: options.transient,
|
|
375
360
|
progressbar: options.progressbar,
|
|
376
361
|
},
|
|
377
|
-
(taskId) => {
|
|
378
|
-
return Effect.forEach(iterable, (item, index) =>
|
|
379
|
-
Effect.tap(f(item, index), () => advanceTask(taskId, 1)),
|
|
380
|
-
);
|
|
381
|
-
},
|
|
382
362
|
);
|
|
383
363
|
|
|
384
364
|
const service: ProgressService = {
|
|
@@ -388,7 +368,6 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
388
368
|
completeTask,
|
|
389
369
|
failTask,
|
|
390
370
|
log,
|
|
391
|
-
withCapturedLogs,
|
|
392
371
|
getTask,
|
|
393
372
|
listTasks,
|
|
394
373
|
withTask,
|
|
@@ -397,3 +376,23 @@ export const makeProgressService = Effect.gen(function* () {
|
|
|
397
376
|
|
|
398
377
|
return Progress.of(service);
|
|
399
378
|
});
|
|
379
|
+
|
|
380
|
+
export class Progress extends Context.Tag("stromseng.dev/Progress")<Progress, ProgressService>() {
|
|
381
|
+
static readonly Default = Layer.scoped(Progress, makeProgressService);
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export const provideProgressService = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
385
|
+
Effect.gen(function* () {
|
|
386
|
+
const existing = yield* Effect.serviceOption(Progress);
|
|
387
|
+
if (Option.isSome(existing)) {
|
|
388
|
+
return yield* Effect.provideService(effect, Progress, existing.value);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const existingTerminal = yield* Effect.serviceOption(ProgressTerminal);
|
|
392
|
+
if (Option.isSome(existingTerminal)) {
|
|
393
|
+
return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const defaultLayers = Layer.provide(Progress.Default, ProgressTerminal.Default);
|
|
397
|
+
return yield* Effect.scoped(effect.pipe(Effect.provide(defaultLayers)));
|
|
398
|
+
});
|
package/src/terminal.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Context, Effect, Layer } from "effect";
|
|
2
|
+
|
|
3
|
+
export interface ProgressTerminalService {
|
|
4
|
+
readonly isTTY: Effect.Effect<boolean>;
|
|
5
|
+
readonly stderrRows: Effect.Effect<number | undefined>;
|
|
6
|
+
readonly stderrColumns: Effect.Effect<number | undefined>;
|
|
7
|
+
readonly writeStderr: (text: string) => Effect.Effect<void>;
|
|
8
|
+
readonly withRawInputCapture: <A, E, R>(
|
|
9
|
+
effect: Effect.Effect<A, E, R>,
|
|
10
|
+
) => Effect.Effect<A, E, R>;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const withRawInputCapture: ProgressTerminalService["withRawInputCapture"] = (effect) =>
|
|
14
|
+
Effect.suspend(() => {
|
|
15
|
+
if (!process.stdin.isTTY) {
|
|
16
|
+
return effect;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const stdin = process.stdin;
|
|
20
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
21
|
+
const onData = (chunk: Buffer) => {
|
|
22
|
+
if (chunk.length === 1 && chunk[0] === 3) {
|
|
23
|
+
process.kill(process.pid, "SIGINT");
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
return Effect.acquireUseRelease(
|
|
28
|
+
Effect.sync(() => {
|
|
29
|
+
stdin.resume();
|
|
30
|
+
stdin.setRawMode?.(true);
|
|
31
|
+
stdin.on("data", onData);
|
|
32
|
+
}),
|
|
33
|
+
() => effect,
|
|
34
|
+
() =>
|
|
35
|
+
Effect.sync(() => {
|
|
36
|
+
try {
|
|
37
|
+
stdin.off("data", onData);
|
|
38
|
+
stdin.setRawMode?.(wasRaw);
|
|
39
|
+
stdin.pause();
|
|
40
|
+
} catch {
|
|
41
|
+
// Best effort terminal restoration.
|
|
42
|
+
}
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export class ProgressTerminal extends Context.Tag("stromseng.dev/ProgressTerminal")<
|
|
48
|
+
ProgressTerminal,
|
|
49
|
+
ProgressTerminalService
|
|
50
|
+
>() {
|
|
51
|
+
static readonly Default = Layer.succeed(ProgressTerminal, {
|
|
52
|
+
isTTY: Effect.sync(() => Boolean(process.stderr.isTTY)),
|
|
53
|
+
stderrRows: Effect.sync(() => process.stderr.rows),
|
|
54
|
+
stderrColumns: Effect.sync(() => process.stderr.columns),
|
|
55
|
+
writeStderr: (text) =>
|
|
56
|
+
Effect.sync(() => {
|
|
57
|
+
process.stderr.write(text);
|
|
58
|
+
}),
|
|
59
|
+
withRawInputCapture,
|
|
60
|
+
} satisfies ProgressTerminalService);
|
|
61
|
+
}
|
package/src/types.ts
CHANGED
|
@@ -3,7 +3,6 @@ import type { PartialDeep } from "type-fest";
|
|
|
3
3
|
import { defaultProgressBarColors, ProgressBarColorsSchema } from "./colors";
|
|
4
4
|
|
|
5
5
|
export const RendererConfigSchema = Schema.Struct({
|
|
6
|
-
isTTY: Schema.Boolean,
|
|
7
6
|
disableUserInput: Schema.Boolean,
|
|
8
7
|
renderIntervalMillis: Schema.Number,
|
|
9
8
|
maxLogLines: Schema.optional(Schema.Number),
|
|
@@ -25,7 +24,6 @@ export type ProgressBarConfigShape = typeof ProgressBarConfigSchema.Type;
|
|
|
25
24
|
export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarConfigSchema);
|
|
26
25
|
|
|
27
26
|
export const defaultRendererConfig: RendererConfigShape = {
|
|
28
|
-
isTTY: Boolean(process.stderr.isTTY),
|
|
29
27
|
disableUserInput: true,
|
|
30
28
|
renderIntervalMillis: 50, // 20 FPS
|
|
31
29
|
maxLogLines: 0,
|
|
@@ -114,21 +112,25 @@ export interface ProgressService {
|
|
|
114
112
|
readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
115
113
|
readonly failTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
116
114
|
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>;
|
|
118
115
|
readonly getTask: (taskId: TaskId) => Effect.Effect<Option.Option<TaskSnapshot>>;
|
|
119
116
|
readonly listTasks: Effect.Effect<ReadonlyArray<TaskSnapshot>>;
|
|
120
|
-
readonly withTask:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
117
|
+
readonly withTask: {
|
|
118
|
+
<A, E, R>(
|
|
119
|
+
effect: Effect.Effect<A, E, R>,
|
|
120
|
+
options: AddTaskOptions,
|
|
121
|
+
): Effect.Effect<A, E, Exclude<R, Task>>;
|
|
122
|
+
<A, E, R>(
|
|
123
|
+
options: AddTaskOptions,
|
|
124
|
+
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Task>>;
|
|
125
|
+
};
|
|
124
126
|
readonly trackIterable: <A, B, E, R>(
|
|
125
127
|
iterable: Iterable<A>,
|
|
126
128
|
options: TrackOptions,
|
|
127
129
|
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
128
|
-
) => Effect.Effect<ReadonlyArray<B>, E, R
|
|
130
|
+
) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Task>>;
|
|
129
131
|
}
|
|
130
132
|
|
|
131
|
-
export class
|
|
133
|
+
export class Task extends Context.Tag("stromseng.dev/Task")<Task, TaskId>() {}
|
|
132
134
|
|
|
133
135
|
export class TaskAddedEvent extends Schema.TaggedClass<TaskAddedEvent>()("TaskAdded", {
|
|
134
136
|
taskId: TaskIdSchema,
|