effective-progress 0.5.3 → 0.5.4
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 +23 -31
- package/dist/index.d.mts +29 -18
- package/dist/index.mjs +370 -127
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
<img alt="Showcase output" src="docs/images/showcase.gif" width="600" />
|
|
13
13
|
|
|
14
|
-
`effective-progress` is an [Effect](https://effect.website/)-
|
|
14
|
+
`effective-progress` is an [Effect](https://effect.website/)-native CLI progress bar library with:
|
|
15
15
|
|
|
16
16
|
- multiple nested tree-like progress bars
|
|
17
17
|
- spinner support for “we have no idea how long this takes” work
|
|
@@ -79,10 +79,17 @@ Effect.runPromise(program);
|
|
|
79
79
|
|
|
80
80
|
<img alt="Nested example output" src="docs/images/nesting.gif" width="600" />
|
|
81
81
|
|
|
82
|
+
### Effect.all modes
|
|
83
|
+
|
|
84
|
+
We support the `either`/`validate` modes of `Effect.all` and render the amount of sucesses/failures.
|
|
85
|
+
|
|
86
|
+
<img alt="Mixed outcomes modes output" src="docs/images/mixedOutcomes.gif" width="600" />
|
|
87
|
+
|
|
82
88
|
### Other examples
|
|
83
89
|
|
|
84
90
|
- `examples/simpleExample.ts` - low-boilerplate real-world flow
|
|
85
91
|
- `examples/advancedExample.ts` - full API usage and manual task control
|
|
92
|
+
- `examples/mixedOutcomes.ts` - fail-fast vs `either`/`validate` with mixed success/failure counters
|
|
86
93
|
- `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
|
|
87
94
|
- `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
|
|
88
95
|
|
|
@@ -98,11 +105,20 @@ Effect.runPromise(program);
|
|
|
98
105
|
|
|
99
106
|
- Rendering is powered by [Ink](https://github.com/vadimdemedes/ink).
|
|
100
107
|
- Built-in columns are: description, bar, amount/spinner, elapsed, and ETA.
|
|
108
|
+
- Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
|
|
109
|
+
- Determinate amount text shows counters without prefixes: `<succeeded> <failed> <processed>/<total>`.
|
|
101
110
|
- Column widths are shared per frame (widest visible cell wins), so rows stay aligned.
|
|
102
111
|
- Elapsed and ETA reserve stable widths to reduce jitter while tasks transition states.
|
|
103
112
|
- Layout uses a 100-column baseline and grows when content requires more space.
|
|
104
113
|
- On narrow terminals, layout compacts to fit available width and tree prefixes are suppressed when description space is too tight.
|
|
105
114
|
|
|
115
|
+
### Mixed outcomes and `mode`
|
|
116
|
+
|
|
117
|
+
- `Progress.all` in default mode (`mode: "default"`) remains fail-fast.
|
|
118
|
+
- In fail-fast runs, unresolved units remain unprocessed.
|
|
119
|
+
- `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
|
|
120
|
+
- Mixed outcomes can still finalize as `done` when all units are accounted for.
|
|
121
|
+
|
|
106
122
|
## Manual task control
|
|
107
123
|
|
|
108
124
|
For manual usage, `task` still provides the current `Task` context, while logs continue through your outer `Console`:
|
|
@@ -110,11 +126,16 @@ For manual usage, `task` still provides the current `Task` context, while logs c
|
|
|
110
126
|
```ts
|
|
111
127
|
const program = Progress.task(
|
|
112
128
|
Effect.gen(function* () {
|
|
129
|
+
const progress = yield* Progress.Progress;
|
|
113
130
|
const currentTask = yield* Progress.Task;
|
|
114
131
|
yield* Console.log("This log is handled by the outer Console", { taskId: currentTask });
|
|
132
|
+
|
|
133
|
+
// Manual determinate updates:
|
|
134
|
+
yield* progress.advanceTask(currentTask, 3);
|
|
135
|
+
yield* progress.advanceTaskFailed(currentTask, 1);
|
|
115
136
|
yield* Effect.sleep("1 second");
|
|
116
137
|
}),
|
|
117
|
-
{ description: "Manual task" },
|
|
138
|
+
{ description: "Manual task", total: 10 },
|
|
118
139
|
);
|
|
119
140
|
```
|
|
120
141
|
|
|
@@ -122,35 +143,6 @@ const program = Progress.task(
|
|
|
122
143
|
|
|
123
144
|
Custom column APIs are not part of the first Ink release. The renderer ships with built-in columns only, and old renderer config APIs (`RendererConfig`, `ProgressBarConfig`, custom column definitions) are intentionally removed in this iteration.
|
|
124
145
|
|
|
125
|
-
## Terminal service and mocking
|
|
126
|
-
|
|
127
|
-
`effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
|
|
128
|
-
|
|
129
|
-
- `isTTY`
|
|
130
|
-
- `stderrRows`
|
|
131
|
-
- `stderrColumns`
|
|
132
|
-
- `writeStderr(text)`
|
|
133
|
-
- `withRawInputCapture(effect)`
|
|
134
|
-
|
|
135
|
-
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:
|
|
136
|
-
|
|
137
|
-
```ts
|
|
138
|
-
import { Effect } from "effect";
|
|
139
|
-
import * as Progress from "effective-progress";
|
|
140
|
-
|
|
141
|
-
const mockTerminal: Progress.ProgressTerminalService = {
|
|
142
|
-
isTTY: Effect.succeed(true),
|
|
143
|
-
stderrRows: Effect.succeed(40),
|
|
144
|
-
stderrColumns: Effect.succeed(120),
|
|
145
|
-
writeStderr: (_text) => Effect.void,
|
|
146
|
-
withRawInputCapture: (effect) => effect,
|
|
147
|
-
};
|
|
148
|
-
|
|
149
|
-
const program = Progress.task(Effect.sleep("100 millis"), { description: "work" }).pipe(
|
|
150
|
-
Effect.provideService(Progress.ProgressTerminal, mockTerminal),
|
|
151
|
-
);
|
|
152
|
-
```
|
|
153
|
-
|
|
154
146
|
## Notes
|
|
155
147
|
|
|
156
148
|
- 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/dist/index.d.mts
CHANGED
|
@@ -8,23 +8,30 @@ type TaskId = typeof TaskIdSchema.Type;
|
|
|
8
8
|
declare const TaskId: Brand.Brand.Constructor<number & Brand.Brand<"TaskId">>;
|
|
9
9
|
declare const TaskStatusSchema: Schema.Literal<["running", "done", "failed"]>;
|
|
10
10
|
type TaskStatus = typeof TaskStatusSchema.Type;
|
|
11
|
+
declare const TaskCountDisplaySchema: Schema.Literal<["processedOnly", "detailed"]>;
|
|
12
|
+
type TaskCountDisplay = typeof TaskCountDisplaySchema.Type;
|
|
11
13
|
interface AddTaskOptions {
|
|
12
14
|
readonly description: string;
|
|
13
15
|
readonly total?: number;
|
|
14
16
|
readonly transient?: boolean;
|
|
15
17
|
readonly parentId?: TaskId;
|
|
18
|
+
readonly countDisplay?: TaskCountDisplay;
|
|
16
19
|
}
|
|
17
20
|
interface UpdateTaskOptions {
|
|
18
21
|
readonly description?: string;
|
|
19
|
-
readonly
|
|
22
|
+
readonly succeeded?: number;
|
|
23
|
+
readonly failed?: number;
|
|
20
24
|
readonly total?: number;
|
|
21
25
|
readonly transient?: boolean;
|
|
26
|
+
readonly countDisplay?: TaskCountDisplay;
|
|
22
27
|
}
|
|
23
28
|
type TrackOptions = Exclude<AddTaskOptions, "parentId">;
|
|
24
29
|
declare const DeterminateTaskUnits_base: Schema.TaggedClass<DeterminateTaskUnits, "DeterminateTaskUnits", {
|
|
25
30
|
readonly _tag: Schema.tag<"DeterminateTaskUnits">;
|
|
26
31
|
} & {
|
|
27
|
-
|
|
32
|
+
succeeded: typeof Schema.Number;
|
|
33
|
+
failed: typeof Schema.Number;
|
|
34
|
+
processed: typeof Schema.Number;
|
|
28
35
|
total: typeof Schema.Number;
|
|
29
36
|
}>;
|
|
30
37
|
declare class DeterminateTaskUnits extends DeterminateTaskUnits_base {}
|
|
@@ -43,6 +50,7 @@ declare const TaskSnapshot_base: Schema.TaggedClass<TaskSnapshot, "TaskSnapshot"
|
|
|
43
50
|
parentId: Schema.NullOr<Schema.brand<typeof Schema.Number, "TaskId">>;
|
|
44
51
|
description: typeof Schema.String;
|
|
45
52
|
status: Schema.Literal<["running", "done", "failed"]>;
|
|
53
|
+
countDisplay: Schema.Literal<["processedOnly", "detailed"]>;
|
|
46
54
|
transient: typeof Schema.Boolean;
|
|
47
55
|
units: Schema.Union<[typeof DeterminateTaskUnits, typeof IndeterminateTaskUnits]>;
|
|
48
56
|
startedAt: typeof Schema.Number;
|
|
@@ -61,6 +69,7 @@ interface ProgressService {
|
|
|
61
69
|
readonly addTask: (options: AddTaskOptions) => Effect.Effect<TaskId>;
|
|
62
70
|
readonly updateTask: (taskId: TaskId, options: UpdateTaskOptions) => Effect.Effect<void>;
|
|
63
71
|
readonly advanceTask: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
|
|
72
|
+
readonly advanceTaskFailed: (taskId: TaskId, amount?: number) => Effect.Effect<void>;
|
|
64
73
|
readonly completeTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
65
74
|
readonly failTask: (taskId: TaskId) => Effect.Effect<void>;
|
|
66
75
|
readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void>;
|
|
@@ -85,6 +94,7 @@ declare const TaskAddedEvent_base: Schema.TaggedClass<TaskAddedEvent, "TaskAdded
|
|
|
85
94
|
description: typeof Schema.String;
|
|
86
95
|
total: Schema.optional<typeof Schema.Number>;
|
|
87
96
|
transient: typeof Schema.Boolean;
|
|
97
|
+
countDisplay: Schema.Literal<["processedOnly", "detailed"]>;
|
|
88
98
|
}>;
|
|
89
99
|
declare class TaskAddedEvent extends TaskAddedEvent_base {}
|
|
90
100
|
declare const TaskUpdatedEvent_base: Schema.TaggedClass<TaskUpdatedEvent, "TaskUpdated", {
|
|
@@ -92,9 +102,12 @@ declare const TaskUpdatedEvent_base: Schema.TaggedClass<TaskUpdatedEvent, "TaskU
|
|
|
92
102
|
} & {
|
|
93
103
|
taskId: Schema.brand<typeof Schema.Number, "TaskId">;
|
|
94
104
|
description: Schema.optional<typeof Schema.String>;
|
|
95
|
-
|
|
105
|
+
succeeded: Schema.optional<typeof Schema.Number>;
|
|
106
|
+
failed: Schema.optional<typeof Schema.Number>;
|
|
107
|
+
processed: Schema.optional<typeof Schema.Number>;
|
|
96
108
|
total: Schema.optional<typeof Schema.Number>;
|
|
97
109
|
transient: Schema.optional<typeof Schema.Boolean>;
|
|
110
|
+
countDisplay: Schema.optional<Schema.Literal<["processedOnly", "detailed"]>>;
|
|
98
111
|
}>;
|
|
99
112
|
declare class TaskUpdatedEvent extends TaskUpdatedEvent_base {}
|
|
100
113
|
declare const TaskAdvancedEvent_base: Schema.TaggedClass<TaskAdvancedEvent, "TaskAdvanced", {
|
|
@@ -102,6 +115,7 @@ declare const TaskAdvancedEvent_base: Schema.TaggedClass<TaskAdvancedEvent, "Tas
|
|
|
102
115
|
} & {
|
|
103
116
|
taskId: Schema.brand<typeof Schema.Number, "TaskId">;
|
|
104
117
|
amount: typeof Schema.Number;
|
|
118
|
+
kind: Schema.Literal<["succeeded", "failed"]>;
|
|
105
119
|
}>;
|
|
106
120
|
declare class TaskAdvancedEvent extends TaskAdvancedEvent_base {}
|
|
107
121
|
declare const TaskCompletedEvent_base: Schema.TaggedClass<TaskCompletedEvent, "TaskCompleted", {
|
|
@@ -142,12 +156,12 @@ interface EffectAllExecutionOptions extends EffectExecutionOptions {
|
|
|
142
156
|
readonly discard?: boolean;
|
|
143
157
|
readonly mode?: "default" | "validate" | "either";
|
|
144
158
|
}
|
|
145
|
-
type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
|
|
159
|
+
type AllOptions = Omit<TrackOptions, "total" | "countDisplay"> & EffectAllExecutionOptions;
|
|
146
160
|
type AllReturn<Arg extends ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>, O extends EffectAllExecutionOptions> = [[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>] ? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : [Arg] extends [Record<string, Effect.Effect<any, any, any>>] ? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>> : never] extends [Effect.Effect<infer A, infer E, infer R>] ? Effect.Effect<A, E, Exclude<R, Progress | Task>> : never;
|
|
147
161
|
interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
148
162
|
readonly discard?: false | undefined;
|
|
149
163
|
}
|
|
150
|
-
type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
164
|
+
type ForEachOptions = Omit<TrackOptions, "countDisplay"> & ForEachExecutionOptions;
|
|
151
165
|
type TaskOptions = AddTaskOptions;
|
|
152
166
|
declare const task: {
|
|
153
167
|
<A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
@@ -155,25 +169,22 @@ declare const task: {
|
|
|
155
169
|
};
|
|
156
170
|
type AllArg = ReadonlyArray<Effect.Effect<any, any, any>> | Record<string, Effect.Effect<any, any, any>>;
|
|
157
171
|
declare const all: {
|
|
158
|
-
<const Arg extends AllArg, O extends EffectAllExecutionOptions>(effects: Arg, options: Omit<TrackOptions, "total"> & O): AllReturn<Arg, O>;
|
|
159
|
-
<O extends EffectAllExecutionOptions>(options: Omit<TrackOptions, "total"> & O): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
|
|
172
|
+
<const Arg extends AllArg, O extends EffectAllExecutionOptions>(effects: Arg, options: Omit<TrackOptions, "total" | "countDisplay"> & O): AllReturn<Arg, O>;
|
|
173
|
+
<O extends EffectAllExecutionOptions>(options: Omit<TrackOptions, "total" | "countDisplay"> & O): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
|
|
160
174
|
};
|
|
161
175
|
declare const forEach: {
|
|
162
176
|
<A, B, E, R>(iterable: Iterable<A>, f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
163
177
|
<A, B, E, R>(f: (item: A, index: number) => Effect.Effect<B, E, R>, options: ForEachOptions): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
164
178
|
};
|
|
165
179
|
//#endregion
|
|
166
|
-
//#region src/
|
|
167
|
-
interface
|
|
168
|
-
readonly
|
|
169
|
-
readonly
|
|
170
|
-
readonly stderrColumns: Effect.Effect<number | undefined>;
|
|
171
|
-
readonly writeStderr: (text: string) => Effect.Effect<void>;
|
|
172
|
-
readonly withRawInputCapture: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>;
|
|
180
|
+
//#region src/stdio.d.ts
|
|
181
|
+
interface ProgressStdioService {
|
|
182
|
+
readonly stdout: NodeJS.WriteStream;
|
|
183
|
+
readonly stderr: NodeJS.WriteStream;
|
|
173
184
|
}
|
|
174
|
-
declare const
|
|
175
|
-
declare class
|
|
176
|
-
static readonly Default: Layer.Layer<
|
|
185
|
+
declare const ProgressStdio_base: Context.TagClass<ProgressStdio, "stromseng.dev/effective-progress/ProgressStdio", ProgressStdioService>;
|
|
186
|
+
declare class ProgressStdio extends ProgressStdio_base {
|
|
187
|
+
static readonly Default: Layer.Layer<ProgressStdio, never, never>;
|
|
177
188
|
}
|
|
178
189
|
//#endregion
|
|
179
|
-
export { AddTaskOptions, AllOptions, AllReturn, DeterminateTaskUnits, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, IndeterminateTaskUnits, Progress, ProgressService,
|
|
190
|
+
export { AddTaskOptions, AllOptions, AllReturn, DeterminateTaskUnits, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, IndeterminateTaskUnits, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
|
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { Brand, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref, Schema } from "effect";
|
|
1
|
+
import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref, Schema } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
|
-
import { Writable } from "node:stream";
|
|
4
3
|
import { Box, Text, render } from "ink";
|
|
5
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
6
5
|
|
|
7
6
|
//#region src/ink-renderer/format.ts
|
|
8
7
|
const SPINNER_FRAMES = [
|
|
@@ -34,19 +33,64 @@ const formatElapsed = (task, now) => {
|
|
|
34
33
|
};
|
|
35
34
|
const formatEta = (task, now) => {
|
|
36
35
|
if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") return "";
|
|
37
|
-
const {
|
|
38
|
-
const remaining = total -
|
|
39
|
-
if (
|
|
36
|
+
const { processed, total } = task.units;
|
|
37
|
+
const remaining = total - processed;
|
|
38
|
+
if (processed <= 0 || remaining <= 0) return "";
|
|
40
39
|
const elapsedMillis = Math.max(1, now - task.startedAt);
|
|
41
|
-
return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis /
|
|
40
|
+
return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / processed * remaining)) / 1e3);
|
|
42
41
|
};
|
|
43
|
-
const
|
|
42
|
+
const getTaskIndicator = (task, tick) => {
|
|
43
|
+
if (task.status === "running") return {
|
|
44
|
+
symbol: SPINNER_FRAMES[tick % SPINNER_FRAMES.length] ?? SPINNER_FRAMES[0],
|
|
45
|
+
color: "yellow"
|
|
46
|
+
};
|
|
47
|
+
if (task.status === "failed") return {
|
|
48
|
+
symbol: "✗",
|
|
49
|
+
color: "red"
|
|
50
|
+
};
|
|
51
|
+
if (task.units._tag !== "DeterminateTaskUnits") return {
|
|
52
|
+
symbol: "✓",
|
|
53
|
+
color: "green"
|
|
54
|
+
};
|
|
55
|
+
const { succeeded, failed, processed, total } = task.units;
|
|
56
|
+
if (failed === 0 && processed === total) return {
|
|
57
|
+
symbol: "✓",
|
|
58
|
+
color: "green"
|
|
59
|
+
};
|
|
60
|
+
if (failed > 0 && succeeded > 0) return {
|
|
61
|
+
symbol: "~",
|
|
62
|
+
color: "yellow"
|
|
63
|
+
};
|
|
64
|
+
if (failed > 0 && succeeded === 0) return {
|
|
65
|
+
symbol: "✗",
|
|
66
|
+
color: "red"
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
symbol: "✓",
|
|
70
|
+
color: "green"
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
const formatDeterminateAmountParts = (task) => {
|
|
74
|
+
if (task.units._tag !== "DeterminateTaskUnits") return;
|
|
75
|
+
const totalText = `${task.units.total}`;
|
|
76
|
+
const width = totalText.length;
|
|
77
|
+
const processedText = `${task.units.processed}`;
|
|
78
|
+
return {
|
|
79
|
+
succeeded: task.countDisplay === "detailed" ? `${task.units.succeeded}`.padStart(width, " ") : "",
|
|
80
|
+
failed: task.countDisplay === "detailed" ? `${task.units.failed}`.padStart(width, " ") : "",
|
|
81
|
+
processed: processedText,
|
|
82
|
+
total: totalText
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const formatAmount = (task, _tick) => {
|
|
44
86
|
if (task.units._tag === "DeterminateTaskUnits") {
|
|
45
|
-
const
|
|
46
|
-
|
|
87
|
+
const parts = formatDeterminateAmountParts(task);
|
|
88
|
+
if (parts === void 0) return "";
|
|
89
|
+
if (task.countDisplay === "detailed") return `${parts.succeeded} ${parts.failed} ${parts.processed}/${parts.total}`;
|
|
90
|
+
return `${parts.processed}/${parts.total}`;
|
|
47
91
|
}
|
|
48
|
-
if (task.status === "running"
|
|
49
|
-
return task.status === "
|
|
92
|
+
if (task.status === "running" && task.units._tag === "IndeterminateTaskUnits") return "";
|
|
93
|
+
return task.status === "failed" ? "✗" : "";
|
|
50
94
|
};
|
|
51
95
|
|
|
52
96
|
//#endregion
|
|
@@ -90,11 +134,11 @@ const computeTreeInfo = (ordered) => {
|
|
|
90
134
|
|
|
91
135
|
//#endregion
|
|
92
136
|
//#region src/ink-renderer/layout.ts
|
|
93
|
-
const DEFAULT_BAR_WIDTH =
|
|
137
|
+
const DEFAULT_BAR_WIDTH = 30;
|
|
94
138
|
const MIN_DESCRIPTION_WIDTH = 8;
|
|
95
139
|
const MIN_BAR_WIDTH = 8;
|
|
96
|
-
const MIN_ELAPSED_WIDTH =
|
|
97
|
-
const MIN_AMOUNT_WIDTH =
|
|
140
|
+
const MIN_ELAPSED_WIDTH = 2;
|
|
141
|
+
const MIN_AMOUNT_WIDTH = 0;
|
|
98
142
|
const BASELINE_ROW_WIDTH = 100;
|
|
99
143
|
const MIN_DESCRIPTION_COLUMNS_FOR_TREE = 24;
|
|
100
144
|
const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
|
|
@@ -102,29 +146,49 @@ const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
|
|
|
102
146
|
const textWidth = (text) => Array.from(text).length;
|
|
103
147
|
const computeWidths = (rows, now, tick, terminalColumns, includeTree = true) => {
|
|
104
148
|
let hasDeterminate = false;
|
|
149
|
+
let hasDetailedDeterminate = false;
|
|
105
150
|
let description = MIN_DESCRIPTION_WIDTH;
|
|
106
|
-
let amount =
|
|
107
|
-
let
|
|
108
|
-
let
|
|
151
|
+
let amount = 0;
|
|
152
|
+
let amountSucceeded = 0;
|
|
153
|
+
let amountFailed = 0;
|
|
154
|
+
let amountProcessed = 0;
|
|
155
|
+
let amountTotal = 0;
|
|
156
|
+
let elapsedContentWidth = MIN_ELAPSED_WIDTH;
|
|
157
|
+
let etaContentWidth = 0;
|
|
109
158
|
for (const row of rows) {
|
|
110
159
|
const { task, tree } = row;
|
|
111
160
|
const treePrefix = includeTree ? renderTreePrefix(tree) : "";
|
|
112
|
-
description = Math.max(description, textWidth(`${treePrefix}${task.description}`));
|
|
113
|
-
if (task.units._tag === "DeterminateTaskUnits")
|
|
114
|
-
|
|
115
|
-
|
|
161
|
+
description = Math.max(description, textWidth(`${treePrefix}${task.description}`) + 2);
|
|
162
|
+
if (task.units._tag === "DeterminateTaskUnits") {
|
|
163
|
+
hasDeterminate = true;
|
|
164
|
+
const totalDigits = textWidth(`${task.units.total}`);
|
|
165
|
+
if (task.countDisplay === "detailed") {
|
|
166
|
+
hasDetailedDeterminate = true;
|
|
167
|
+
amountSucceeded = Math.max(amountSucceeded, totalDigits);
|
|
168
|
+
amountFailed = Math.max(amountFailed, totalDigits);
|
|
169
|
+
}
|
|
170
|
+
amountProcessed = Math.max(amountProcessed, totalDigits);
|
|
171
|
+
amountTotal = Math.max(amountTotal, totalDigits);
|
|
172
|
+
} else amount = Math.max(amount, textWidth(formatAmount(task, tick)));
|
|
173
|
+
elapsedContentWidth = Math.max(elapsedContentWidth, textWidth(formatElapsed(task, now)));
|
|
116
174
|
if (task.status === "running" && task.units._tag === "DeterminateTaskUnits") {
|
|
117
175
|
const etaValue = formatEta(task, now);
|
|
118
176
|
const etaText = `ETA: ${etaValue.length > 0 ? etaValue : "--"}`;
|
|
119
|
-
|
|
177
|
+
etaContentWidth = Math.max(etaContentWidth, textWidth(etaText));
|
|
120
178
|
}
|
|
121
179
|
}
|
|
180
|
+
const structuredAmount = hasDeterminate ? amountProcessed + 1 + amountTotal + (hasDetailedDeterminate ? amountSucceeded + 1 + amountFailed + 1 : 0) : 0;
|
|
181
|
+
if (hasDeterminate) amount = structuredAmount;
|
|
122
182
|
let widths = {
|
|
123
183
|
description,
|
|
124
184
|
bar: hasDeterminate ? DEFAULT_BAR_WIDTH : 0,
|
|
125
185
|
amount,
|
|
126
|
-
|
|
127
|
-
|
|
186
|
+
amountSucceeded,
|
|
187
|
+
amountFailed,
|
|
188
|
+
amountProcessed,
|
|
189
|
+
amountTotal,
|
|
190
|
+
elapsed: hasDeterminate ? Math.max(elapsedContentWidth, RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR) : elapsedContentWidth,
|
|
191
|
+
eta: etaContentWidth > 0 ? Math.max(etaContentWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR) : 0
|
|
128
192
|
};
|
|
129
193
|
const visible = (w) => [
|
|
130
194
|
w.description,
|
|
@@ -137,7 +201,7 @@ const computeWidths = (rows, now, tick, terminalColumns, includeTree = true) =>
|
|
|
137
201
|
const cols = visible(w);
|
|
138
202
|
return cols.reduce((sum, width) => sum + width, 0) + Math.max(0, cols.length - 1);
|
|
139
203
|
};
|
|
140
|
-
const baselineTarget = Math.max(BASELINE_ROW_WIDTH, total(widths));
|
|
204
|
+
const baselineTarget = hasDeterminate ? Math.max(BASELINE_ROW_WIDTH, total(widths)) : total(widths);
|
|
141
205
|
const target = terminalColumns === void 0 ? baselineTarget : Math.max(1, Math.min(Math.max(1, Math.floor(terminalColumns)), baselineTarget));
|
|
142
206
|
if (total(widths) < target) widths.description += target - total(widths);
|
|
143
207
|
else if (total(widths) > target) {
|
|
@@ -154,20 +218,28 @@ const computeWidths = (rows, now, tick, terminalColumns, includeTree = true) =>
|
|
|
154
218
|
};
|
|
155
219
|
overflow -= delta;
|
|
156
220
|
};
|
|
157
|
-
reduceBy("
|
|
158
|
-
reduceBy("
|
|
221
|
+
reduceBy("eta", etaContentWidth);
|
|
222
|
+
reduceBy("elapsed", elapsedContentWidth);
|
|
159
223
|
reduceBy("bar", MIN_BAR_WIDTH);
|
|
224
|
+
reduceBy("eta", 0);
|
|
160
225
|
reduceBy("bar", 0);
|
|
161
226
|
reduceBy("elapsed", MIN_ELAPSED_WIDTH);
|
|
227
|
+
reduceBy("description", MIN_DESCRIPTION_WIDTH);
|
|
162
228
|
reduceBy("amount", MIN_AMOUNT_WIDTH);
|
|
163
229
|
reduceBy("description", 0);
|
|
164
230
|
if (total(widths) < target) widths.description += target - total(widths);
|
|
165
231
|
}
|
|
232
|
+
const rowWidth = total(widths);
|
|
233
|
+
const useStructuredAmount = hasDeterminate && widths.amount >= structuredAmount;
|
|
166
234
|
return {
|
|
167
|
-
row:
|
|
235
|
+
row: rowWidth,
|
|
168
236
|
description: widths.description,
|
|
169
237
|
bar: widths.bar,
|
|
170
238
|
amount: widths.amount,
|
|
239
|
+
amountSucceeded: useStructuredAmount ? widths.amountSucceeded : 0,
|
|
240
|
+
amountFailed: useStructuredAmount ? widths.amountFailed : 0,
|
|
241
|
+
amountProcessed: useStructuredAmount ? widths.amountProcessed : 0,
|
|
242
|
+
amountTotal: useStructuredAmount ? widths.amountTotal : 0,
|
|
171
243
|
elapsed: widths.elapsed,
|
|
172
244
|
eta: widths.eta
|
|
173
245
|
};
|
|
@@ -186,42 +258,161 @@ const computeSharedColumnWidths = (rows, now, tick, terminalColumns) => {
|
|
|
186
258
|
|
|
187
259
|
//#endregion
|
|
188
260
|
//#region src/ink-renderer/columns/amount-column.tsx
|
|
189
|
-
const
|
|
190
|
-
|
|
261
|
+
const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
|
|
262
|
+
const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
|
|
263
|
+
const blank = (width) => " ".repeat(Math.max(0, width));
|
|
264
|
+
const shouldShowDetailedCounts = (task) => task.units._tag === "DeterminateTaskUnits" && task.countDisplay === "detailed";
|
|
265
|
+
const SucceededCountColumn = ({ task, width }) => {
|
|
266
|
+
if (width <= 0) return null;
|
|
267
|
+
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
268
|
+
if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
191
269
|
return /* @__PURE__ */ jsx(Text, {
|
|
192
|
-
color:
|
|
193
|
-
children:
|
|
270
|
+
color: "green",
|
|
271
|
+
children: padLeft(`${task.units.succeeded}`, width)
|
|
272
|
+
});
|
|
273
|
+
};
|
|
274
|
+
const FailedCountColumn = ({ task, width }) => {
|
|
275
|
+
if (width <= 0) return null;
|
|
276
|
+
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
277
|
+
if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
278
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
279
|
+
color: "red",
|
|
280
|
+
children: padLeft(`${task.units.failed}`, width)
|
|
281
|
+
});
|
|
282
|
+
};
|
|
283
|
+
const ProcessedCountColumn = ({ task, width }) => {
|
|
284
|
+
if (width <= 0) return null;
|
|
285
|
+
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
286
|
+
return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
|
|
287
|
+
};
|
|
288
|
+
const AmountSeparatorColumn = ({ task, showStructured, tick }) => {
|
|
289
|
+
if (!showStructured) return null;
|
|
290
|
+
if (task.units._tag === "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: "/" });
|
|
291
|
+
const symbol = formatAmount(task, tick);
|
|
292
|
+
if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
|
|
293
|
+
color: "red",
|
|
294
|
+
children: symbol
|
|
295
|
+
});
|
|
296
|
+
if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
|
|
297
|
+
color: "yellow",
|
|
298
|
+
children: symbol
|
|
299
|
+
});
|
|
300
|
+
return /* @__PURE__ */ jsx(Text, { children: symbol });
|
|
301
|
+
};
|
|
302
|
+
const TotalCountColumn = ({ task, width }) => {
|
|
303
|
+
if (width <= 0) return null;
|
|
304
|
+
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
305
|
+
return /* @__PURE__ */ jsx(Text, { children: padRight(`${task.units.total}`, width) });
|
|
306
|
+
};
|
|
307
|
+
const AmountColumn = ({ task, tick, amountSucceededWidth, amountFailedWidth, amountProcessedWidth, amountTotalWidth }) => {
|
|
308
|
+
const showStructured = amountProcessedWidth > 0 && amountTotalWidth > 0;
|
|
309
|
+
if (!showStructured) {
|
|
310
|
+
const text = formatAmount(task, tick);
|
|
311
|
+
if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
|
|
312
|
+
wrap: "truncate-end",
|
|
313
|
+
color: "red",
|
|
314
|
+
children: text
|
|
315
|
+
});
|
|
316
|
+
if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
|
|
317
|
+
wrap: "truncate-end",
|
|
318
|
+
color: "yellow",
|
|
319
|
+
children: text
|
|
320
|
+
});
|
|
321
|
+
return /* @__PURE__ */ jsx(Text, {
|
|
322
|
+
wrap: "truncate-end",
|
|
323
|
+
children: text
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return /* @__PURE__ */ jsxs(Box, {
|
|
327
|
+
flexDirection: "row",
|
|
328
|
+
children: [
|
|
329
|
+
amountSucceededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SucceededCountColumn, {
|
|
330
|
+
task,
|
|
331
|
+
width: amountSucceededWidth
|
|
332
|
+
}), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
|
|
333
|
+
amountFailedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FailedCountColumn, {
|
|
334
|
+
task,
|
|
335
|
+
width: amountFailedWidth
|
|
336
|
+
}), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
|
|
337
|
+
/* @__PURE__ */ jsx(ProcessedCountColumn, {
|
|
338
|
+
task,
|
|
339
|
+
width: amountProcessedWidth
|
|
340
|
+
}),
|
|
341
|
+
/* @__PURE__ */ jsx(AmountSeparatorColumn, {
|
|
342
|
+
task,
|
|
343
|
+
showStructured,
|
|
344
|
+
tick
|
|
345
|
+
}),
|
|
346
|
+
/* @__PURE__ */ jsx(TotalCountColumn, {
|
|
347
|
+
task,
|
|
348
|
+
width: amountTotalWidth
|
|
349
|
+
})
|
|
350
|
+
]
|
|
194
351
|
});
|
|
195
352
|
};
|
|
196
353
|
|
|
197
354
|
//#endregion
|
|
198
355
|
//#region src/ink-renderer/columns/bar-column.tsx
|
|
199
|
-
const
|
|
356
|
+
const segmentLengths = (width, total, succeeded, failed) => {
|
|
357
|
+
if (total <= 0) return {
|
|
358
|
+
succeeded: 0,
|
|
359
|
+
failed: 0,
|
|
360
|
+
remaining: width
|
|
361
|
+
};
|
|
362
|
+
const succeededEnd = Math.round(succeeded / total * width);
|
|
363
|
+
const failedEnd = Math.round((succeeded + failed) / total * width);
|
|
364
|
+
const succeededLength = Math.max(0, Math.min(width, succeededEnd));
|
|
365
|
+
const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
|
|
366
|
+
return {
|
|
367
|
+
succeeded: succeededLength,
|
|
368
|
+
failed: failedLength,
|
|
369
|
+
remaining: Math.max(0, width - succeededLength - failedLength)
|
|
370
|
+
};
|
|
371
|
+
};
|
|
200
372
|
const BarColumn = ({ task, width }) => {
|
|
201
373
|
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
const ratio = task.status === "done" ? 1 : clamp(task.units.completed / safeTotal, 0, 1);
|
|
205
|
-
const filled = Math.round(barWidth * ratio);
|
|
206
|
-
const empty = Math.max(0, barWidth - filled);
|
|
207
|
-
const bar = `${"━".repeat(filled)}${"─".repeat(empty)}`;
|
|
208
|
-
return /* @__PURE__ */ jsx(Text, {
|
|
374
|
+
const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
|
|
375
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
209
376
|
wrap: "truncate-end",
|
|
210
|
-
|
|
211
|
-
|
|
377
|
+
children: [
|
|
378
|
+
/* @__PURE__ */ jsx(Text, {
|
|
379
|
+
color: "green",
|
|
380
|
+
children: "━".repeat(lengths.succeeded)
|
|
381
|
+
}),
|
|
382
|
+
/* @__PURE__ */ jsx(Text, {
|
|
383
|
+
color: "red",
|
|
384
|
+
children: "━".repeat(lengths.failed)
|
|
385
|
+
}),
|
|
386
|
+
/* @__PURE__ */ jsx(Text, {
|
|
387
|
+
color: "gray",
|
|
388
|
+
children: "─".repeat(lengths.remaining)
|
|
389
|
+
})
|
|
390
|
+
]
|
|
212
391
|
});
|
|
213
392
|
};
|
|
214
393
|
|
|
215
394
|
//#endregion
|
|
216
395
|
//#region src/ink-renderer/columns/description-column.tsx
|
|
217
|
-
const DescriptionColumn = ({ task, tree, showTree }) =>
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
396
|
+
const DescriptionColumn = ({ task, tree, showTree, tick }) => {
|
|
397
|
+
const treePrefix = showTree ? renderTreePrefix(tree) : "";
|
|
398
|
+
const indicator = getTaskIndicator(task, tick);
|
|
399
|
+
return /* @__PURE__ */ jsxs(Text, {
|
|
400
|
+
wrap: "truncate-end",
|
|
401
|
+
children: [
|
|
402
|
+
treePrefix,
|
|
403
|
+
/* @__PURE__ */ jsx(Text, {
|
|
404
|
+
color: indicator.color,
|
|
405
|
+
children: indicator.symbol
|
|
406
|
+
}),
|
|
407
|
+
` ${task.description}`
|
|
408
|
+
]
|
|
409
|
+
});
|
|
410
|
+
};
|
|
221
411
|
|
|
222
412
|
//#endregion
|
|
223
413
|
//#region src/ink-renderer/columns/elapsed-column.tsx
|
|
224
414
|
const ElapsedColumn = ({ task, now }) => /* @__PURE__ */ jsx(Text, {
|
|
415
|
+
wrap: "truncate-end",
|
|
225
416
|
color: "gray",
|
|
226
417
|
children: formatElapsed(task, now)
|
|
227
418
|
});
|
|
@@ -247,7 +438,11 @@ const TaskRow = ({ row, now, tick, isTTY, widths }) => {
|
|
|
247
438
|
now,
|
|
248
439
|
tick,
|
|
249
440
|
isTTY,
|
|
250
|
-
showTree: widths.showTree
|
|
441
|
+
showTree: widths.showTree,
|
|
442
|
+
amountSucceededWidth: widths.amountSucceeded,
|
|
443
|
+
amountFailedWidth: widths.amountFailed,
|
|
444
|
+
amountProcessedWidth: widths.amountProcessed,
|
|
445
|
+
amountTotalWidth: widths.amountTotal
|
|
251
446
|
};
|
|
252
447
|
return /* @__PURE__ */ jsxs(Box, {
|
|
253
448
|
flexDirection: "row",
|
|
@@ -268,12 +463,12 @@ const TaskRow = ({ row, now, tick, isTTY, widths }) => {
|
|
|
268
463
|
width: Math.max(1, Math.min(widths.bar, DEFAULT_BAR_WIDTH))
|
|
269
464
|
})
|
|
270
465
|
}) : null,
|
|
271
|
-
/* @__PURE__ */ jsx(Box, {
|
|
466
|
+
widths.amount > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
272
467
|
width: widths.amount,
|
|
273
468
|
flexShrink: 0,
|
|
274
469
|
marginRight: 1,
|
|
275
470
|
children: /* @__PURE__ */ jsx(AmountColumn, { ...props })
|
|
276
|
-
}),
|
|
471
|
+
}) : null,
|
|
277
472
|
/* @__PURE__ */ jsx(Box, {
|
|
278
473
|
width: widths.elapsed,
|
|
279
474
|
flexShrink: 0,
|
|
@@ -324,17 +519,7 @@ const toTaskRows = (store) => computeTreeInfo(orderedVisibleTasks(store)).map((e
|
|
|
324
519
|
//#region src/ink-renderer/service.tsx
|
|
325
520
|
const RENDER_INTERVAL_MILLIS = 100;
|
|
326
521
|
const hasRunningSpinners = (tasks) => tasks.some((task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits");
|
|
327
|
-
const
|
|
328
|
-
try {
|
|
329
|
-
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : `${chunk}`;
|
|
330
|
-
Effect.runSync(terminal.writeStderr(text));
|
|
331
|
-
callback();
|
|
332
|
-
} catch (error) {
|
|
333
|
-
callback(error);
|
|
334
|
-
}
|
|
335
|
-
} });
|
|
336
|
-
const makeDefaultInkRenderer = () => ({ run: (storeRef, dirtyRef, terminal, isTTY) => Effect.gen(function* () {
|
|
337
|
-
const output = createInkWritable(terminal);
|
|
522
|
+
const makeDefaultInkRenderer = () => ({ run: (storeRef, dirtyRef, stdio, isTTY) => Effect.gen(function* () {
|
|
338
523
|
let instance;
|
|
339
524
|
let tick = 0;
|
|
340
525
|
let rendererActive = false;
|
|
@@ -348,8 +533,8 @@ const makeDefaultInkRenderer = () => ({ run: (storeRef, dirtyRef, terminal, isTT
|
|
|
348
533
|
});
|
|
349
534
|
if (instance === void 0) {
|
|
350
535
|
instance = render(app, {
|
|
351
|
-
stdout:
|
|
352
|
-
stderr:
|
|
536
|
+
stdout: stdio.stdout,
|
|
537
|
+
stderr: stdio.stderr,
|
|
353
538
|
patchConsole: true,
|
|
354
539
|
exitOnCtrlC: false,
|
|
355
540
|
debug: false
|
|
@@ -364,12 +549,12 @@ const makeDefaultInkRenderer = () => ({ run: (storeRef, dirtyRef, terminal, isTT
|
|
|
364
549
|
const dirty = yield* Ref.getAndSet(dirtyRef, false);
|
|
365
550
|
const store = yield* Ref.get(storeRef);
|
|
366
551
|
const tasks = Array.from(store.tasks.values()).filter((task) => !(task.transient && task.status !== "running"));
|
|
367
|
-
if (dirty || hasRunningSpinners(tasks)) yield* renderStore(store, yield* Clock.currentTimeMillis, isTTY ?
|
|
552
|
+
if (dirty || hasRunningSpinners(tasks)) yield* renderStore(store, yield* Clock.currentTimeMillis, isTTY ? stdio.stderr.columns : void 0);
|
|
368
553
|
tick += 1;
|
|
369
554
|
yield* Effect.sleep(RENDER_INTERVAL_MILLIS);
|
|
370
555
|
}
|
|
371
556
|
}).pipe(Effect.ensuring(Effect.gen(function* () {
|
|
372
|
-
if (rendererActive) yield* renderStore(yield* Ref.get(storeRef), yield* Clock.currentTimeMillis, isTTY ?
|
|
557
|
+
if (rendererActive) yield* renderStore(yield* Ref.get(storeRef), yield* Clock.currentTimeMillis, isTTY ? stdio.stderr.columns : void 0);
|
|
373
558
|
yield* Effect.sync(() => {
|
|
374
559
|
instance?.unmount();
|
|
375
560
|
});
|
|
@@ -380,37 +565,13 @@ var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective
|
|
|
380
565
|
};
|
|
381
566
|
|
|
382
567
|
//#endregion
|
|
383
|
-
//#region src/
|
|
384
|
-
const
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
const wasRaw = Boolean(stdin.isRaw);
|
|
388
|
-
const onData = (chunk) => {
|
|
389
|
-
if (chunk.length === 1 && chunk[0] === 3) process.kill(process.pid, "SIGINT");
|
|
390
|
-
};
|
|
391
|
-
return Effect.acquireUseRelease(Effect.sync(() => {
|
|
392
|
-
stdin.resume();
|
|
393
|
-
stdin.setRawMode?.(true);
|
|
394
|
-
stdin.on("data", onData);
|
|
395
|
-
}), () => effect, () => Effect.sync(() => {
|
|
396
|
-
try {
|
|
397
|
-
stdin.off("data", onData);
|
|
398
|
-
stdin.setRawMode?.(wasRaw);
|
|
399
|
-
stdin.pause();
|
|
400
|
-
} catch {}
|
|
401
|
-
}));
|
|
402
|
-
});
|
|
403
|
-
const defaultTerminalService = {
|
|
404
|
-
isTTY: Effect.sync(() => Boolean(process.stderr.isTTY)),
|
|
405
|
-
stderrRows: Effect.sync(() => process.stderr.rows),
|
|
406
|
-
stderrColumns: Effect.sync(() => process.stderr.columns),
|
|
407
|
-
writeStderr: (text) => Effect.sync(() => {
|
|
408
|
-
process.stderr.write(text);
|
|
409
|
-
}),
|
|
410
|
-
withRawInputCapture
|
|
568
|
+
//#region src/stdio.ts
|
|
569
|
+
const defaultStdioService = {
|
|
570
|
+
stdout: process.stdout,
|
|
571
|
+
stderr: process.stderr
|
|
411
572
|
};
|
|
412
|
-
var
|
|
413
|
-
static Default = Layer.succeed(
|
|
573
|
+
var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effective-progress/ProgressStdio")() {
|
|
574
|
+
static Default = Layer.succeed(ProgressStdio, defaultStdioService);
|
|
414
575
|
};
|
|
415
576
|
|
|
416
577
|
//#endregion
|
|
@@ -418,8 +579,11 @@ var ProgressTerminal = class ProgressTerminal extends Context.Tag("stromseng.dev
|
|
|
418
579
|
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
419
580
|
const TaskId = Brand.nominal();
|
|
420
581
|
const TaskStatusSchema = Schema.Literal("running", "done", "failed");
|
|
582
|
+
const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
|
|
421
583
|
var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
|
|
422
|
-
|
|
584
|
+
succeeded: Schema.Number,
|
|
585
|
+
failed: Schema.Number,
|
|
586
|
+
processed: Schema.Number,
|
|
423
587
|
total: Schema.Number
|
|
424
588
|
}) {};
|
|
425
589
|
var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", { spinnerFrame: Schema.Number }) {};
|
|
@@ -429,6 +593,7 @@ var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
|
|
|
429
593
|
parentId: Schema.NullOr(TaskIdSchema),
|
|
430
594
|
description: Schema.String,
|
|
431
595
|
status: TaskStatusSchema,
|
|
596
|
+
countDisplay: TaskCountDisplaySchema,
|
|
432
597
|
transient: Schema.Boolean,
|
|
433
598
|
units: TaskUnitsSchema,
|
|
434
599
|
startedAt: Schema.Number,
|
|
@@ -440,18 +605,23 @@ var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
|
|
|
440
605
|
parentId: Schema.NullOr(TaskIdSchema),
|
|
441
606
|
description: Schema.String,
|
|
442
607
|
total: Schema.optional(Schema.Number),
|
|
443
|
-
transient: Schema.Boolean
|
|
608
|
+
transient: Schema.Boolean,
|
|
609
|
+
countDisplay: TaskCountDisplaySchema
|
|
444
610
|
}) {};
|
|
445
611
|
var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
|
|
446
612
|
taskId: TaskIdSchema,
|
|
447
613
|
description: Schema.optional(Schema.String),
|
|
448
|
-
|
|
614
|
+
succeeded: Schema.optional(Schema.Number),
|
|
615
|
+
failed: Schema.optional(Schema.Number),
|
|
616
|
+
processed: Schema.optional(Schema.Number),
|
|
449
617
|
total: Schema.optional(Schema.Number),
|
|
450
|
-
transient: Schema.optional(Schema.Boolean)
|
|
618
|
+
transient: Schema.optional(Schema.Boolean),
|
|
619
|
+
countDisplay: Schema.optional(TaskCountDisplaySchema)
|
|
451
620
|
}) {};
|
|
452
621
|
var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
|
|
453
622
|
taskId: TaskIdSchema,
|
|
454
|
-
amount: Schema.Number
|
|
623
|
+
amount: Schema.Number,
|
|
624
|
+
kind: Schema.Literal("succeeded", "failed")
|
|
455
625
|
}) {};
|
|
456
626
|
var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
|
|
457
627
|
var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
|
|
@@ -461,23 +631,37 @@ const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema
|
|
|
461
631
|
|
|
462
632
|
//#endregion
|
|
463
633
|
//#region src/runtime.ts
|
|
634
|
+
const normalizeDeterminateCounts = (counts) => {
|
|
635
|
+
const total = Math.max(0, counts.total);
|
|
636
|
+
const failed = Math.min(total, Math.max(0, counts.failed));
|
|
637
|
+
const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
|
|
638
|
+
return new DeterminateTaskUnits({
|
|
639
|
+
succeeded,
|
|
640
|
+
failed,
|
|
641
|
+
processed: succeeded + failed,
|
|
642
|
+
total
|
|
643
|
+
});
|
|
644
|
+
};
|
|
645
|
+
const updateDeterminateCounts = (units, options) => normalizeDeterminateCounts({
|
|
646
|
+
succeeded: options.succeeded ?? units.succeeded,
|
|
647
|
+
failed: options.failed ?? units.failed,
|
|
648
|
+
total: options.total ?? units.total
|
|
649
|
+
});
|
|
464
650
|
const updatedSnapshot = (snapshot, options) => {
|
|
465
651
|
const currentUnits = snapshot.units;
|
|
466
652
|
const units = (() => {
|
|
467
653
|
if (options.total !== void 0) {
|
|
468
654
|
if (options.total <= 0) return new IndeterminateTaskUnits({ spinnerFrame: 0 });
|
|
469
|
-
|
|
470
|
-
return
|
|
471
|
-
|
|
472
|
-
|
|
655
|
+
if (currentUnits._tag === "DeterminateTaskUnits") return updateDeterminateCounts(currentUnits, options);
|
|
656
|
+
return normalizeDeterminateCounts({
|
|
657
|
+
succeeded: options.succeeded ?? 0,
|
|
658
|
+
failed: options.failed ?? 0,
|
|
659
|
+
total: options.total
|
|
473
660
|
});
|
|
474
661
|
}
|
|
475
662
|
if (currentUnits._tag === "DeterminateTaskUnits") {
|
|
476
|
-
if (options.
|
|
477
|
-
return
|
|
478
|
-
completed: Math.max(0, options.completed),
|
|
479
|
-
total: currentUnits.total
|
|
480
|
-
});
|
|
663
|
+
if (options.succeeded === void 0 && options.failed === void 0) return currentUnits;
|
|
664
|
+
return updateDeterminateCounts(currentUnits, options);
|
|
481
665
|
}
|
|
482
666
|
return currentUnits;
|
|
483
667
|
})();
|
|
@@ -486,6 +670,7 @@ const updatedSnapshot = (snapshot, options) => {
|
|
|
486
670
|
parentId: snapshot.parentId,
|
|
487
671
|
description: options.description ?? snapshot.description,
|
|
488
672
|
status: snapshot.status,
|
|
673
|
+
countDisplay: options.countDisplay ?? snapshot.countDisplay,
|
|
489
674
|
transient: options.transient ?? snapshot.transient,
|
|
490
675
|
units,
|
|
491
676
|
startedAt: snapshot.startedAt,
|
|
@@ -497,6 +682,7 @@ const withTransient = (snapshot, transient) => new TaskSnapshot({
|
|
|
497
682
|
parentId: snapshot.parentId,
|
|
498
683
|
description: snapshot.description,
|
|
499
684
|
status: snapshot.status,
|
|
685
|
+
countDisplay: snapshot.countDisplay,
|
|
500
686
|
transient,
|
|
501
687
|
units: snapshot.units,
|
|
502
688
|
startedAt: snapshot.startedAt,
|
|
@@ -531,10 +717,10 @@ const removeFromRenderOrder = (renderOrder, taskId) => {
|
|
|
531
717
|
return next;
|
|
532
718
|
};
|
|
533
719
|
const makeProgressService = Effect.gen(function* () {
|
|
534
|
-
const
|
|
720
|
+
const stdio = yield* ProgressStdio;
|
|
535
721
|
const inkRenderer = yield* InkRenderer;
|
|
536
722
|
const outerConsole = yield* Effect.console;
|
|
537
|
-
const isTTY =
|
|
723
|
+
const isTTY = Boolean(stdio.stderr.isTTY);
|
|
538
724
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
539
725
|
const storeRef = yield* Ref.make({
|
|
540
726
|
tasks: /* @__PURE__ */ new Map(),
|
|
@@ -545,25 +731,28 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
545
731
|
const scope = yield* Effect.scope;
|
|
546
732
|
const markDirty = Ref.set(dirtyRef, true);
|
|
547
733
|
const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
|
|
548
|
-
yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef,
|
|
734
|
+
yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef, stdio, isTTY), scope);
|
|
549
735
|
yield* Effect.sleep("0 millis");
|
|
550
736
|
const addTask = (options) => Effect.gen(function* () {
|
|
551
737
|
const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
|
|
552
738
|
const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
|
|
553
|
-
const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({ spinnerFrame: 0 }) :
|
|
554
|
-
|
|
555
|
-
|
|
739
|
+
const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({ spinnerFrame: 0 }) : normalizeDeterminateCounts({
|
|
740
|
+
succeeded: 0,
|
|
741
|
+
failed: 0,
|
|
742
|
+
total: options.total
|
|
556
743
|
});
|
|
557
744
|
const store = yield* Ref.get(storeRef);
|
|
558
745
|
const parentSnapshot = Option.isSome(resolvedParentId) ? store.tasks.get(resolvedParentId.value) : void 0;
|
|
559
746
|
const now = yield* Clock.currentTimeMillis;
|
|
560
747
|
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
748
|
+
const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
|
|
561
749
|
const snapshot = new TaskSnapshot({
|
|
562
750
|
id: taskId,
|
|
563
751
|
parentId: parentIdValue,
|
|
564
752
|
description: options.description,
|
|
565
753
|
status: "running",
|
|
566
|
-
|
|
754
|
+
countDisplay,
|
|
755
|
+
transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
|
|
567
756
|
units,
|
|
568
757
|
startedAt: now,
|
|
569
758
|
completedAt: null
|
|
@@ -612,8 +801,9 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
612
801
|
const advanceTask = (taskId, amount = 1) => Ref.update(storeRef, (store) => {
|
|
613
802
|
const snapshot = store.tasks.get(taskId);
|
|
614
803
|
if (!snapshot) return store;
|
|
615
|
-
const units = snapshot.units._tag === "DeterminateTaskUnits" ?
|
|
616
|
-
|
|
804
|
+
const units = snapshot.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
|
|
805
|
+
succeeded: snapshot.units.succeeded + amount,
|
|
806
|
+
failed: snapshot.units.failed,
|
|
617
807
|
total: snapshot.units.total
|
|
618
808
|
}) : new IndeterminateTaskUnits({ spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount) });
|
|
619
809
|
const nextTasks = new Map(store.tasks);
|
|
@@ -622,6 +812,33 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
622
812
|
parentId: snapshot.parentId,
|
|
623
813
|
description: snapshot.description,
|
|
624
814
|
status: snapshot.status,
|
|
815
|
+
countDisplay: snapshot.countDisplay,
|
|
816
|
+
transient: snapshot.transient,
|
|
817
|
+
units,
|
|
818
|
+
startedAt: snapshot.startedAt,
|
|
819
|
+
completedAt: snapshot.completedAt
|
|
820
|
+
}));
|
|
821
|
+
return {
|
|
822
|
+
tasks: nextTasks,
|
|
823
|
+
renderOrder: store.renderOrder
|
|
824
|
+
};
|
|
825
|
+
}).pipe(Effect.zipRight(markDirty));
|
|
826
|
+
const advanceTaskFailed = (taskId, amount = 1) => Ref.update(storeRef, (store) => {
|
|
827
|
+
const snapshot = store.tasks.get(taskId);
|
|
828
|
+
if (!snapshot) return store;
|
|
829
|
+
if (snapshot.units._tag !== "DeterminateTaskUnits") return store;
|
|
830
|
+
const units = normalizeDeterminateCounts({
|
|
831
|
+
succeeded: snapshot.units.succeeded,
|
|
832
|
+
failed: snapshot.units.failed + amount,
|
|
833
|
+
total: snapshot.units.total
|
|
834
|
+
});
|
|
835
|
+
const nextTasks = new Map(store.tasks);
|
|
836
|
+
nextTasks.set(taskId, new TaskSnapshot({
|
|
837
|
+
id: snapshot.id,
|
|
838
|
+
parentId: snapshot.parentId,
|
|
839
|
+
description: snapshot.description,
|
|
840
|
+
status: snapshot.status,
|
|
841
|
+
countDisplay: snapshot.countDisplay,
|
|
625
842
|
transient: snapshot.transient,
|
|
626
843
|
units,
|
|
627
844
|
startedAt: snapshot.startedAt,
|
|
@@ -650,9 +867,11 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
650
867
|
parentId: snapshot.parentId,
|
|
651
868
|
description: snapshot.description,
|
|
652
869
|
status: "done",
|
|
870
|
+
countDisplay: snapshot.countDisplay,
|
|
653
871
|
transient: snapshot.transient,
|
|
654
|
-
units: snapshot.units._tag === "DeterminateTaskUnits" ?
|
|
655
|
-
|
|
872
|
+
units: snapshot.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
|
|
873
|
+
succeeded: snapshot.units.total - snapshot.units.failed,
|
|
874
|
+
failed: snapshot.units.failed,
|
|
656
875
|
total: snapshot.units.total
|
|
657
876
|
}) : snapshot.units,
|
|
658
877
|
startedAt: snapshot.startedAt,
|
|
@@ -683,6 +902,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
683
902
|
parentId: snapshot.parentId,
|
|
684
903
|
description: snapshot.description,
|
|
685
904
|
status: "failed",
|
|
905
|
+
countDisplay: snapshot.countDisplay,
|
|
686
906
|
transient: snapshot.transient,
|
|
687
907
|
units: snapshot.units,
|
|
688
908
|
startedAt: snapshot.startedAt,
|
|
@@ -711,6 +931,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
711
931
|
addTask,
|
|
712
932
|
updateTask,
|
|
713
933
|
advanceTask,
|
|
934
|
+
advanceTaskFailed,
|
|
714
935
|
completeTask,
|
|
715
936
|
failTask,
|
|
716
937
|
log,
|
|
@@ -732,11 +953,11 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
732
953
|
});
|
|
733
954
|
var Progress = class Progress extends Context.Tag("stromseng.dev/effective-progress/Progress")() {
|
|
734
955
|
static Default = Layer.unwrapEffect(Effect.gen(function* () {
|
|
735
|
-
const
|
|
956
|
+
const stdioOption = yield* Effect.serviceOption(ProgressStdio);
|
|
736
957
|
const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
|
|
737
958
|
let layer = Layer.scoped(Progress, makeProgressService);
|
|
738
959
|
if (Option.isNone(inkRendererOption)) layer = layer.pipe(Layer.provide(InkRenderer.Default));
|
|
739
|
-
if (Option.isNone(
|
|
960
|
+
if (Option.isNone(stdioOption)) layer = layer.pipe(Layer.provide(ProgressStdio.Default));
|
|
740
961
|
return layer;
|
|
741
962
|
}));
|
|
742
963
|
};
|
|
@@ -765,11 +986,29 @@ const task = dual(2, (effect, options) => {
|
|
|
765
986
|
});
|
|
766
987
|
const wrapEffects = (effects, tap) => Array.isArray(effects) ? effects.map(tap) : Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
|
|
767
988
|
const countEffects = (effects) => Array.isArray(effects) ? effects.length : Object.keys(effects).length;
|
|
989
|
+
const isCollectAllMode = (mode) => mode === "either" || mode === "validate";
|
|
990
|
+
const allCountDisplay = (mode) => isCollectAllMode(mode) ? "detailed" : "processedOnly";
|
|
991
|
+
const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* () {
|
|
992
|
+
const exit = yield* Effect.exit(effect);
|
|
993
|
+
if (Exit.isSuccess(exit)) {
|
|
994
|
+
yield* progress.advanceTask(taskId, 1);
|
|
995
|
+
return exit.value;
|
|
996
|
+
}
|
|
997
|
+
if (Cause.isInterruptedOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
|
|
998
|
+
yield* progress.advanceTaskFailed(taskId, 1);
|
|
999
|
+
return yield* Effect.failCause(exit.cause);
|
|
1000
|
+
});
|
|
1001
|
+
const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
|
|
1002
|
+
const taskOption = yield* progress.getTask(taskId);
|
|
1003
|
+
if (Option.isNone(taskOption) || taskOption.value.units._tag !== "DeterminateTaskUnits") return false;
|
|
1004
|
+
const { processed, total } = taskOption.value.units;
|
|
1005
|
+
return processed >= total;
|
|
1006
|
+
});
|
|
768
1007
|
const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
|
|
769
1008
|
const progress = yield* Progress;
|
|
770
1009
|
return yield* progress.runTask(Effect.gen(function* () {
|
|
771
1010
|
const taskId = yield* Task;
|
|
772
|
-
const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) =>
|
|
1011
|
+
const exit = yield* Effect.exit(Effect.all(wrapEffects(effects, (effect) => wrapTrackedEffect(progress, taskId, effect)), {
|
|
773
1012
|
concurrency: options.concurrency,
|
|
774
1013
|
batching: options.batching,
|
|
775
1014
|
discard: options.discard,
|
|
@@ -777,6 +1016,8 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
|
|
|
777
1016
|
concurrentFinalizers: options.concurrentFinalizers
|
|
778
1017
|
}));
|
|
779
1018
|
if (Exit.isSuccess(exit)) yield* progress.completeTask(taskId);
|
|
1019
|
+
else if (!isCollectAllMode(options.mode)) yield* progress.failTask(taskId);
|
|
1020
|
+
else if (yield* isTaskFullyProcessed(progress, taskId)) yield* progress.completeTask(taskId);
|
|
780
1021
|
else yield* progress.failTask(taskId);
|
|
781
1022
|
return yield* Exit.match(exit, {
|
|
782
1023
|
onFailure: Effect.failCause,
|
|
@@ -785,14 +1026,15 @@ const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* (
|
|
|
785
1026
|
}), {
|
|
786
1027
|
description: options.description,
|
|
787
1028
|
total: countEffects(effects),
|
|
788
|
-
transient: options.transient
|
|
1029
|
+
transient: options.transient,
|
|
1030
|
+
countDisplay: allCountDisplay(options.mode)
|
|
789
1031
|
});
|
|
790
1032
|
})));
|
|
791
1033
|
const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(function* () {
|
|
792
1034
|
const progress = yield* Progress;
|
|
793
1035
|
return yield* progress.runTask(Effect.gen(function* () {
|
|
794
1036
|
const taskId = yield* Task;
|
|
795
|
-
const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) =>
|
|
1037
|
+
const exit = yield* Effect.exit(Effect.forEach(iterable, (item, index) => wrapTrackedEffect(progress, taskId, f(item, index)), {
|
|
796
1038
|
concurrency: options.concurrency,
|
|
797
1039
|
batching: options.batching,
|
|
798
1040
|
discard: options.discard,
|
|
@@ -807,9 +1049,10 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
|
|
|
807
1049
|
}), {
|
|
808
1050
|
description: options.description,
|
|
809
1051
|
total: options.total ?? inferTotal(iterable),
|
|
810
|
-
transient: options.transient
|
|
1052
|
+
transient: options.transient,
|
|
1053
|
+
countDisplay: "processedOnly"
|
|
811
1054
|
});
|
|
812
1055
|
})));
|
|
813
1056
|
|
|
814
1057
|
//#endregion
|
|
815
|
-
export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress,
|
|
1058
|
+
export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
|
package/package.json
CHANGED