effective-progress 0.4.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -106
- package/package.json +4 -1
- package/src/api.ts +11 -49
- package/src/index.ts +0 -1
- package/src/ink-renderer/app.tsx +31 -0
- package/src/ink-renderer/columns/amount-column.tsx +17 -0
- package/src/ink-renderer/columns/bar-column.tsx +31 -0
- package/src/ink-renderer/columns/description-column.tsx +7 -0
- package/src/ink-renderer/columns/elapsed-column.tsx +7 -0
- package/src/ink-renderer/columns/eta-column.tsx +18 -0
- package/src/ink-renderer/columns/index.ts +6 -0
- package/src/ink-renderer/columns/types.ts +11 -0
- package/src/ink-renderer/format.ts +55 -0
- package/src/ink-renderer/index.ts +1 -0
- package/src/ink-renderer/layout.ts +145 -0
- package/src/ink-renderer/model.ts +24 -0
- package/src/ink-renderer/service.tsx +121 -0
- package/src/ink-renderer/task-row.tsx +53 -0
- package/src/{renderer → ink-renderer}/tree.ts +3 -9
- package/src/ink-renderer/types.ts +18 -0
- package/src/runtime.ts +9 -83
- package/src/types.ts +0 -70
- package/src/console.ts +0 -177
- package/src/renderer/ansi.ts +0 -138
- package/src/renderer/types.ts +0 -67
- package/src/renderer.ts +0 -1246
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { Writable } from "node:stream";
|
|
2
|
+
import { Clock, Context, Effect, Layer, Ref } from "effect";
|
|
3
|
+
import { render, type Instance } from "ink";
|
|
4
|
+
import type { ProgressTerminalService } from "../terminal";
|
|
5
|
+
import type { TaskSnapshot, TaskStore } from "../types";
|
|
6
|
+
import { ProgressApp } from "./app";
|
|
7
|
+
import { toTaskRows } from "./model";
|
|
8
|
+
|
|
9
|
+
const RENDER_INTERVAL_MILLIS = 100;
|
|
10
|
+
|
|
11
|
+
export interface InkRendererService {
|
|
12
|
+
readonly run: (
|
|
13
|
+
storeRef: Ref.Ref<TaskStore>,
|
|
14
|
+
dirtyRef: Ref.Ref<boolean>,
|
|
15
|
+
terminal: ProgressTerminalService,
|
|
16
|
+
isTTY: boolean,
|
|
17
|
+
) => Effect.Effect<void>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const hasRunningSpinners = (tasks: ReadonlyArray<TaskSnapshot>): boolean =>
|
|
21
|
+
tasks.some(
|
|
22
|
+
(task) => task.status === "running" && task.units._tag === "IndeterminateTaskUnits",
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
const createInkWritable = (terminal: ProgressTerminalService): Writable =>
|
|
26
|
+
new Writable({
|
|
27
|
+
write(chunk, _encoding, callback) {
|
|
28
|
+
try {
|
|
29
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : `${chunk}`;
|
|
30
|
+
Effect.runSync(terminal.writeStderr(text));
|
|
31
|
+
callback();
|
|
32
|
+
} catch (error) {
|
|
33
|
+
callback(error as Error);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const makeDefaultInkRenderer = (): InkRendererService => ({
|
|
39
|
+
run: (storeRef, dirtyRef, terminal, isTTY) =>
|
|
40
|
+
Effect.gen(function* () {
|
|
41
|
+
const output = createInkWritable(terminal);
|
|
42
|
+
let instance: Instance | undefined;
|
|
43
|
+
let tick = 0;
|
|
44
|
+
let rendererActive = false;
|
|
45
|
+
|
|
46
|
+
const renderStore = (
|
|
47
|
+
store: TaskStore,
|
|
48
|
+
now: number,
|
|
49
|
+
terminalColumns: number | undefined,
|
|
50
|
+
) =>
|
|
51
|
+
Effect.sync(() => {
|
|
52
|
+
const app = (
|
|
53
|
+
<ProgressApp
|
|
54
|
+
rows={toTaskRows(store)}
|
|
55
|
+
now={now}
|
|
56
|
+
tick={tick}
|
|
57
|
+
isTTY={isTTY}
|
|
58
|
+
terminalColumns={terminalColumns}
|
|
59
|
+
/>
|
|
60
|
+
);
|
|
61
|
+
if (instance === undefined) {
|
|
62
|
+
instance = render(app, {
|
|
63
|
+
stdout: output as unknown as NodeJS.WriteStream,
|
|
64
|
+
stderr: output as unknown as NodeJS.WriteStream,
|
|
65
|
+
patchConsole: true,
|
|
66
|
+
exitOnCtrlC: false,
|
|
67
|
+
debug: false,
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
instance.rerender(app);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const renderLoop = Effect.gen(function* () {
|
|
76
|
+
rendererActive = true;
|
|
77
|
+
|
|
78
|
+
while (true) {
|
|
79
|
+
const dirty = yield* Ref.getAndSet(dirtyRef, false);
|
|
80
|
+
const store = yield* Ref.get(storeRef);
|
|
81
|
+
const tasks = Array.from(store.tasks.values()).filter(
|
|
82
|
+
(task) => !(task.transient && task.status !== "running"),
|
|
83
|
+
);
|
|
84
|
+
const shouldRender = dirty || hasRunningSpinners(tasks);
|
|
85
|
+
|
|
86
|
+
if (shouldRender) {
|
|
87
|
+
const now = yield* Clock.currentTimeMillis;
|
|
88
|
+
const terminalColumns = isTTY ? yield* terminal.stderrColumns : undefined;
|
|
89
|
+
yield* renderStore(store, now, terminalColumns);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
tick += 1;
|
|
93
|
+
yield* Effect.sleep(RENDER_INTERVAL_MILLIS);
|
|
94
|
+
}
|
|
95
|
+
}).pipe(
|
|
96
|
+
Effect.ensuring(
|
|
97
|
+
Effect.gen(function* () {
|
|
98
|
+
if (rendererActive) {
|
|
99
|
+
const store = yield* Ref.get(storeRef);
|
|
100
|
+
const now = yield* Clock.currentTimeMillis;
|
|
101
|
+
const terminalColumns = isTTY ? yield* terminal.stderrColumns : undefined;
|
|
102
|
+
yield* renderStore(store, now, terminalColumns);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
yield* Effect.sync(() => {
|
|
106
|
+
instance?.unmount();
|
|
107
|
+
});
|
|
108
|
+
}),
|
|
109
|
+
),
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
return yield* renderLoop;
|
|
113
|
+
}),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")<
|
|
117
|
+
InkRenderer,
|
|
118
|
+
InkRendererService
|
|
119
|
+
>() {
|
|
120
|
+
static readonly Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
|
|
121
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Box } from "ink";
|
|
2
|
+
import { DEFAULT_BAR_WIDTH, type SharedColumnWidths } from "./layout";
|
|
3
|
+
import type { TaskRowModel } from "./types";
|
|
4
|
+
import {
|
|
5
|
+
AmountColumn,
|
|
6
|
+
BarColumn,
|
|
7
|
+
DescriptionColumn,
|
|
8
|
+
ElapsedColumn,
|
|
9
|
+
EtaColumn,
|
|
10
|
+
} from "./columns";
|
|
11
|
+
|
|
12
|
+
export interface TaskRowProps {
|
|
13
|
+
readonly row: TaskRowModel;
|
|
14
|
+
readonly now: number;
|
|
15
|
+
readonly tick: number;
|
|
16
|
+
readonly isTTY: boolean;
|
|
17
|
+
readonly widths: SharedColumnWidths;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const TaskRow = ({ row, now, tick, isTTY, widths }: TaskRowProps) => {
|
|
21
|
+
const props = {
|
|
22
|
+
task: row.task,
|
|
23
|
+
tree: row.tree,
|
|
24
|
+
now,
|
|
25
|
+
tick,
|
|
26
|
+
isTTY,
|
|
27
|
+
showTree: widths.showTree,
|
|
28
|
+
} as const;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Box flexDirection="row" minWidth={widths.row}>
|
|
32
|
+
<Box width={widths.description} flexShrink={1} marginRight={1}>
|
|
33
|
+
<DescriptionColumn {...props} />
|
|
34
|
+
</Box>
|
|
35
|
+
{widths.bar > 0 ? (
|
|
36
|
+
<Box width={widths.bar} flexShrink={0} marginRight={1}>
|
|
37
|
+
<BarColumn {...props} width={Math.max(1, Math.min(widths.bar, DEFAULT_BAR_WIDTH))} />
|
|
38
|
+
</Box>
|
|
39
|
+
) : null}
|
|
40
|
+
<Box width={widths.amount} flexShrink={0} marginRight={1}>
|
|
41
|
+
<AmountColumn {...props} />
|
|
42
|
+
</Box>
|
|
43
|
+
<Box width={widths.elapsed} flexShrink={0} marginRight={1}>
|
|
44
|
+
<ElapsedColumn {...props} />
|
|
45
|
+
</Box>
|
|
46
|
+
{widths.eta > 0 ? (
|
|
47
|
+
<Box width={widths.eta} flexShrink={0}>
|
|
48
|
+
<EtaColumn {...props} />
|
|
49
|
+
</Box>
|
|
50
|
+
) : null}
|
|
51
|
+
</Box>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
@@ -1,10 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { TaskTreeInfo } from "./types";
|
|
3
|
-
|
|
4
|
-
export interface OrderedTreeTask {
|
|
5
|
-
readonly snapshot: TaskSnapshot;
|
|
6
|
-
readonly depth: number;
|
|
7
|
-
}
|
|
1
|
+
import type { OrderedTask, TaskTreeInfo } from "./types";
|
|
8
2
|
|
|
9
3
|
const treeAncestorPrefix = (tree: TaskTreeInfo): string =>
|
|
10
4
|
tree.ancestorHasNextSibling
|
|
@@ -22,8 +16,8 @@ export const renderTreePrefix = (tree: TaskTreeInfo): string => {
|
|
|
22
16
|
};
|
|
23
17
|
|
|
24
18
|
export const computeTreeInfo = (
|
|
25
|
-
ordered: ReadonlyArray<
|
|
26
|
-
): ReadonlyArray<
|
|
19
|
+
ordered: ReadonlyArray<OrderedTask>,
|
|
20
|
+
): ReadonlyArray<OrderedTask & { readonly tree: TaskTreeInfo }> => {
|
|
27
21
|
const hasNextSiblingByIndex: Array<boolean> = Array.from({ length: ordered.length }, () => false);
|
|
28
22
|
|
|
29
23
|
for (let i = 0; i < ordered.length; i++) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { TaskSnapshot } from "../types";
|
|
2
|
+
|
|
3
|
+
export interface TaskTreeInfo {
|
|
4
|
+
readonly depth: number;
|
|
5
|
+
readonly hasNextSibling: boolean;
|
|
6
|
+
readonly hasChildren: boolean;
|
|
7
|
+
readonly ancestorHasNextSibling: ReadonlyArray<boolean>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface OrderedTask {
|
|
11
|
+
readonly snapshot: TaskSnapshot;
|
|
12
|
+
readonly depth: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface TaskRowModel {
|
|
16
|
+
readonly task: TaskSnapshot;
|
|
17
|
+
readonly tree: TaskTreeInfo;
|
|
18
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -1,47 +1,22 @@
|
|
|
1
1
|
import { Clock, Context, Effect, Exit, FiberRef, Layer, Option, Ref } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
|
-
import {
|
|
4
|
-
import type { PartialDeep } from "type-fest";
|
|
5
|
-
import { type BufferedConsoleCall, makeConsoleBridge } from "./console";
|
|
6
|
-
import { Columns, FrameRenderer } from "./renderer";
|
|
3
|
+
import { InkRenderer } from "./ink-renderer";
|
|
7
4
|
import { ProgressTerminal } from "./terminal";
|
|
8
5
|
import type {
|
|
9
6
|
AddTaskOptions,
|
|
10
7
|
ProgressService,
|
|
11
8
|
RenderRow,
|
|
12
|
-
RendererConfigShape,
|
|
13
9
|
TaskStore,
|
|
14
10
|
UpdateTaskOptions,
|
|
15
11
|
} from "./types";
|
|
16
12
|
import {
|
|
17
|
-
decodeProgressBarConfigSync,
|
|
18
|
-
decodeRendererConfigSync,
|
|
19
|
-
defaultProgressBarConfig,
|
|
20
|
-
defaultRendererConfig,
|
|
21
13
|
DeterminateTaskUnits,
|
|
22
14
|
IndeterminateTaskUnits,
|
|
23
|
-
ProgressBarConfig,
|
|
24
|
-
RendererConfig,
|
|
25
15
|
Task,
|
|
26
16
|
TaskId,
|
|
27
17
|
TaskSnapshot,
|
|
28
18
|
} from "./types";
|
|
29
19
|
|
|
30
|
-
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
31
|
-
base: T,
|
|
32
|
-
override: PartialDeep<T> | undefined,
|
|
33
|
-
): T =>
|
|
34
|
-
mergeWith(
|
|
35
|
-
structuredClone(base),
|
|
36
|
-
(override ?? {}) as Record<PropertyKey, any>,
|
|
37
|
-
(_targetValue, sourceValue) => {
|
|
38
|
-
if (Array.isArray(sourceValue)) {
|
|
39
|
-
return sourceValue;
|
|
40
|
-
}
|
|
41
|
-
return undefined;
|
|
42
|
-
},
|
|
43
|
-
) as T;
|
|
44
|
-
|
|
45
20
|
const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
|
|
46
21
|
const currentUnits = snapshot.units;
|
|
47
22
|
const units = (() => {
|
|
@@ -81,7 +56,6 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
81
56
|
status: snapshot.status,
|
|
82
57
|
transient: options.transient ?? snapshot.transient,
|
|
83
58
|
units,
|
|
84
|
-
config: snapshot.config,
|
|
85
59
|
startedAt: snapshot.startedAt,
|
|
86
60
|
completedAt: snapshot.completedAt,
|
|
87
61
|
});
|
|
@@ -95,7 +69,6 @@ const withTransient = (snapshot: TaskSnapshot, transient: boolean): TaskSnapshot
|
|
|
95
69
|
status: snapshot.status,
|
|
96
70
|
transient,
|
|
97
71
|
units: snapshot.units,
|
|
98
|
-
config: snapshot.config,
|
|
99
72
|
startedAt: snapshot.startedAt,
|
|
100
73
|
completedAt: snapshot.completedAt,
|
|
101
74
|
});
|
|
@@ -129,33 +102,9 @@ const removeFromRenderOrder = (
|
|
|
129
102
|
return next;
|
|
130
103
|
};
|
|
131
104
|
|
|
132
|
-
const withDefaultColumns = (rendererConfig: RendererConfigShape): RendererConfigShape => ({
|
|
133
|
-
...rendererConfig,
|
|
134
|
-
columns: rendererConfig.columns.length > 0 ? rendererConfig.columns : Columns.defaults(),
|
|
135
|
-
});
|
|
136
|
-
|
|
137
105
|
const makeProgressService = Effect.gen(function* () {
|
|
138
|
-
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
139
|
-
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
140
|
-
|
|
141
|
-
const rendererConfig = withDefaultColumns(
|
|
142
|
-
decodeRendererConfigSync(
|
|
143
|
-
mergeConfig(
|
|
144
|
-
defaultRendererConfig,
|
|
145
|
-
Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
|
|
146
|
-
),
|
|
147
|
-
),
|
|
148
|
-
);
|
|
149
|
-
|
|
150
|
-
const progressBarConfig = decodeProgressBarConfigSync(
|
|
151
|
-
mergeConfig(
|
|
152
|
-
defaultProgressBarConfig,
|
|
153
|
-
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
154
|
-
),
|
|
155
|
-
);
|
|
156
|
-
|
|
157
106
|
const terminal = yield* ProgressTerminal;
|
|
158
|
-
const
|
|
107
|
+
const inkRenderer = yield* InkRenderer;
|
|
159
108
|
const outerConsole = yield* Effect.console;
|
|
160
109
|
const isTTY = yield* terminal.isTTY;
|
|
161
110
|
|
|
@@ -164,30 +113,15 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
164
113
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
165
114
|
renderOrder: [],
|
|
166
115
|
});
|
|
167
|
-
const pendingLogsRef = yield* Ref.make<ReadonlyArray<BufferedConsoleCall>>([]);
|
|
168
116
|
const dirtyRef = yield* Ref.make(true);
|
|
169
117
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
170
118
|
const scope = yield* Effect.scope;
|
|
171
119
|
|
|
172
120
|
const markDirty = Ref.set(dirtyRef, true);
|
|
173
|
-
const
|
|
174
|
-
outerConsole
|
|
175
|
-
pendingLogsRef,
|
|
176
|
-
markDirty,
|
|
177
|
-
);
|
|
121
|
+
const log = (...args: ReadonlyArray<unknown>) =>
|
|
122
|
+
args.length === 0 ? Effect.void : outerConsole.log(...args);
|
|
178
123
|
|
|
179
|
-
yield* Effect.forkIn(
|
|
180
|
-
frameRenderer.run(
|
|
181
|
-
storeRef,
|
|
182
|
-
pendingLogsRef,
|
|
183
|
-
replayLogs,
|
|
184
|
-
dirtyRef,
|
|
185
|
-
terminal,
|
|
186
|
-
isTTY,
|
|
187
|
-
rendererConfig,
|
|
188
|
-
),
|
|
189
|
-
scope,
|
|
190
|
-
);
|
|
124
|
+
yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef, terminal, isTTY), scope);
|
|
191
125
|
// Let the renderer fiber start so queued logs are reliably flushed on scope teardown.
|
|
192
126
|
yield* Effect.sleep("0 millis");
|
|
193
127
|
|
|
@@ -206,10 +140,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
206
140
|
const parentSnapshot = Option.isSome(resolvedParentId)
|
|
207
141
|
? store.tasks.get(resolvedParentId.value)
|
|
208
142
|
: undefined;
|
|
209
|
-
const inheritedProgressBarConfig = parentSnapshot?.config ?? progressBarConfig;
|
|
210
|
-
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
211
|
-
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
212
|
-
);
|
|
213
143
|
|
|
214
144
|
const now = yield* Clock.currentTimeMillis;
|
|
215
145
|
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
@@ -220,7 +150,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
220
150
|
status: "running",
|
|
221
151
|
transient: parentSnapshot?.transient ?? options.transient ?? false,
|
|
222
152
|
units,
|
|
223
|
-
config: resolvedProgressBarConfig,
|
|
224
153
|
startedAt: now,
|
|
225
154
|
completedAt: null,
|
|
226
155
|
});
|
|
@@ -296,7 +225,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
296
225
|
status: snapshot.status,
|
|
297
226
|
transient: snapshot.transient,
|
|
298
227
|
units,
|
|
299
|
-
config: snapshot.config,
|
|
300
228
|
startedAt: snapshot.startedAt,
|
|
301
229
|
completedAt: snapshot.completedAt,
|
|
302
230
|
}),
|
|
@@ -336,7 +264,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
336
264
|
total: snapshot.units.total,
|
|
337
265
|
})
|
|
338
266
|
: snapshot.units,
|
|
339
|
-
config: snapshot.config,
|
|
340
267
|
startedAt: snapshot.startedAt,
|
|
341
268
|
completedAt: now,
|
|
342
269
|
}),
|
|
@@ -371,7 +298,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
371
298
|
status: "failed",
|
|
372
299
|
transient: snapshot.transient,
|
|
373
300
|
units: snapshot.units,
|
|
374
|
-
config: snapshot.config,
|
|
375
301
|
startedAt: snapshot.startedAt,
|
|
376
302
|
completedAt: now,
|
|
377
303
|
}),
|
|
@@ -401,7 +327,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
401
327
|
});
|
|
402
328
|
|
|
403
329
|
return yield* Effect.locally(
|
|
404
|
-
Effect.
|
|
330
|
+
Effect.provideService(effect, Task, taskId),
|
|
405
331
|
currentParentRef,
|
|
406
332
|
Option.some(taskId),
|
|
407
333
|
);
|
|
@@ -454,11 +380,11 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
|
|
|
454
380
|
static readonly Default = Layer.unwrapEffect(
|
|
455
381
|
Effect.gen(function* () {
|
|
456
382
|
const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
|
|
457
|
-
const
|
|
383
|
+
const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
|
|
458
384
|
let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
|
|
459
385
|
|
|
460
|
-
if (Option.isNone(
|
|
461
|
-
layer = layer.pipe(Layer.provide(
|
|
386
|
+
if (Option.isNone(inkRendererOption)) {
|
|
387
|
+
layer = layer.pipe(Layer.provide(InkRenderer.Default));
|
|
462
388
|
}
|
|
463
389
|
if (Option.isNone(terminalOption)) {
|
|
464
390
|
layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
|
package/src/types.ts
CHANGED
|
@@ -1,72 +1,4 @@
|
|
|
1
1
|
import { Brand, Context, Effect, Option, Schema } from "effect";
|
|
2
|
-
import type { PartialDeep } from "type-fest";
|
|
3
|
-
import type { ProgressColumn } from "./renderer";
|
|
4
|
-
|
|
5
|
-
export const RendererConfigSchema = Schema.Struct({
|
|
6
|
-
disableUserInput: Schema.Boolean,
|
|
7
|
-
renderIntervalMillis: Schema.Number,
|
|
8
|
-
nonTtyUpdateStep: Schema.Number,
|
|
9
|
-
width: Schema.Union(Schema.Number, Schema.Literal("fullwidth")),
|
|
10
|
-
columnGap: Schema.Number,
|
|
11
|
-
columns: Schema.Array(Schema.Unknown),
|
|
12
|
-
});
|
|
13
|
-
export type RendererConfigShape = {
|
|
14
|
-
readonly disableUserInput: boolean;
|
|
15
|
-
readonly renderIntervalMillis: number;
|
|
16
|
-
readonly nonTtyUpdateStep: number;
|
|
17
|
-
readonly width: number | "fullwidth";
|
|
18
|
-
readonly columnGap: number;
|
|
19
|
-
readonly columns: ReadonlyArray<ProgressColumn | string>;
|
|
20
|
-
};
|
|
21
|
-
const decodeRendererConfigSchemaSync = Schema.decodeUnknownSync(RendererConfigSchema);
|
|
22
|
-
export const decodeRendererConfigSync = (input: unknown): RendererConfigShape => {
|
|
23
|
-
if (typeof input === "object" && input !== null && "determinateTaskLayout" in input) {
|
|
24
|
-
throw new Error("determinateTaskLayout has been removed. Use columns instead.");
|
|
25
|
-
}
|
|
26
|
-
if (typeof input === "object" && input !== null && "maxTaskWidth" in input) {
|
|
27
|
-
throw new Error("maxTaskWidth has been removed. Use width on RendererConfig instead.");
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
return decodeRendererConfigSchemaSync(input) as RendererConfigShape;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
export const ProgressBarConfigSchema = Schema.Struct({
|
|
34
|
-
spinnerFrames: Schema.NonEmptyArray(Schema.String),
|
|
35
|
-
barWidth: Schema.Number,
|
|
36
|
-
fillChar: Schema.String,
|
|
37
|
-
emptyChar: Schema.String,
|
|
38
|
-
leftBracket: Schema.String,
|
|
39
|
-
rightBracket: Schema.String,
|
|
40
|
-
});
|
|
41
|
-
export type ProgressBarConfigShape = typeof ProgressBarConfigSchema.Type;
|
|
42
|
-
export const decodeProgressBarConfigSync = Schema.decodeUnknownSync(ProgressBarConfigSchema);
|
|
43
|
-
|
|
44
|
-
export const defaultRendererConfig: RendererConfigShape = {
|
|
45
|
-
disableUserInput: true,
|
|
46
|
-
renderIntervalMillis: 100, // 10 FPS
|
|
47
|
-
nonTtyUpdateStep: 5,
|
|
48
|
-
width: 120,
|
|
49
|
-
columnGap: 1,
|
|
50
|
-
columns: [],
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export const defaultProgressBarConfig: ProgressBarConfigShape = {
|
|
54
|
-
spinnerFrames: ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"],
|
|
55
|
-
barWidth: 40,
|
|
56
|
-
fillChar: "━",
|
|
57
|
-
emptyChar: "─",
|
|
58
|
-
leftBracket: "",
|
|
59
|
-
rightBracket: "",
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export class RendererConfig extends Context.Tag("stromseng.dev/effective-progress/RendererConfig")<
|
|
63
|
-
RendererConfig,
|
|
64
|
-
PartialDeep<RendererConfigShape>
|
|
65
|
-
>() {}
|
|
66
|
-
|
|
67
|
-
export class ProgressBarConfig extends Context.Tag(
|
|
68
|
-
"stromseng.dev/effective-progress/ProgressBarConfig",
|
|
69
|
-
)<ProgressBarConfig, PartialDeep<ProgressBarConfigShape>>() {}
|
|
70
2
|
|
|
71
3
|
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
72
4
|
|
|
@@ -82,7 +14,6 @@ export interface AddTaskOptions {
|
|
|
82
14
|
readonly total?: number;
|
|
83
15
|
readonly transient?: boolean;
|
|
84
16
|
readonly parentId?: TaskId;
|
|
85
|
-
readonly progressbar?: PartialDeep<ProgressBarConfigShape>;
|
|
86
17
|
}
|
|
87
18
|
|
|
88
19
|
export interface UpdateTaskOptions {
|
|
@@ -120,7 +51,6 @@ export class TaskSnapshot extends Schema.TaggedClass<TaskSnapshot>()("TaskSnapsh
|
|
|
120
51
|
status: TaskStatusSchema,
|
|
121
52
|
transient: Schema.Boolean,
|
|
122
53
|
units: TaskUnitsSchema,
|
|
123
|
-
config: ProgressBarConfigSchema,
|
|
124
54
|
startedAt: Schema.Number,
|
|
125
55
|
completedAt: Schema.NullOr(Schema.Number),
|
|
126
56
|
}) {}
|
package/src/console.ts
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import { Console, Effect, Ref } from "effect";
|
|
2
|
-
|
|
3
|
-
export type BufferedConsoleMethod =
|
|
4
|
-
| "assert"
|
|
5
|
-
| "debug"
|
|
6
|
-
| "dir"
|
|
7
|
-
| "dirxml"
|
|
8
|
-
| "error"
|
|
9
|
-
| "group"
|
|
10
|
-
| "groupCollapsed"
|
|
11
|
-
| "groupEnd"
|
|
12
|
-
| "info"
|
|
13
|
-
| "log"
|
|
14
|
-
| "table"
|
|
15
|
-
| "trace"
|
|
16
|
-
| "warn";
|
|
17
|
-
|
|
18
|
-
export interface BufferedConsoleCall {
|
|
19
|
-
readonly method: BufferedConsoleMethod;
|
|
20
|
-
readonly args: ReadonlyArray<unknown>;
|
|
21
|
-
readonly unsafe: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ConsoleBridge {
|
|
25
|
-
readonly progressConsole: Console.Console;
|
|
26
|
-
readonly appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>;
|
|
27
|
-
readonly replayLogs: (
|
|
28
|
-
logs: ReadonlyArray<BufferedConsoleCall>,
|
|
29
|
-
) => Effect.Effect<void, never, never>;
|
|
30
|
-
readonly log: (...args: ReadonlyArray<unknown>) => Effect.Effect<void, never, never>;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const replayBufferedConsoleCall = (
|
|
34
|
-
outerConsole: Console.Console,
|
|
35
|
-
call: BufferedConsoleCall,
|
|
36
|
-
): Effect.Effect<void, never, never> => {
|
|
37
|
-
type DirOptions = Parameters<Console.Console["dir"]>[1];
|
|
38
|
-
type GroupOptions = Parameters<Console.Console["group"]>[0];
|
|
39
|
-
|
|
40
|
-
switch (call.method) {
|
|
41
|
-
case "assert": {
|
|
42
|
-
const [condition, ...rest] = call.args;
|
|
43
|
-
return outerConsole.assert(condition as boolean, ...rest);
|
|
44
|
-
}
|
|
45
|
-
case "debug":
|
|
46
|
-
return outerConsole.debug(...call.args);
|
|
47
|
-
case "dir":
|
|
48
|
-
return outerConsole.dir(call.args[0], call.args[1] as DirOptions);
|
|
49
|
-
case "dirxml":
|
|
50
|
-
return outerConsole.dirxml(...call.args);
|
|
51
|
-
case "error":
|
|
52
|
-
return outerConsole.error(...call.args);
|
|
53
|
-
case "group":
|
|
54
|
-
return outerConsole.group(call.args[0] as GroupOptions);
|
|
55
|
-
case "groupCollapsed":
|
|
56
|
-
return outerConsole.group(call.args[0] as GroupOptions);
|
|
57
|
-
case "groupEnd":
|
|
58
|
-
return outerConsole.groupEnd;
|
|
59
|
-
case "info":
|
|
60
|
-
return outerConsole.info(...call.args);
|
|
61
|
-
case "log":
|
|
62
|
-
return outerConsole.log(...call.args);
|
|
63
|
-
case "table":
|
|
64
|
-
return outerConsole.table(call.args[0], call.args[1] as ReadonlyArray<string> | undefined);
|
|
65
|
-
case "trace":
|
|
66
|
-
return outerConsole.trace(...call.args);
|
|
67
|
-
case "warn":
|
|
68
|
-
return outerConsole.warn(...call.args);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
export const makeProgressConsole = (
|
|
73
|
-
appendLog: (call: BufferedConsoleCall) => Effect.Effect<void, never, never>,
|
|
74
|
-
): Console.Console => {
|
|
75
|
-
const log = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) =>
|
|
76
|
-
appendLog({ method, args, unsafe: false });
|
|
77
|
-
const unsafeLog = (method: BufferedConsoleMethod, ...args: ReadonlyArray<unknown>) => {
|
|
78
|
-
Effect.runSync(appendLog({ method, args, unsafe: true }));
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
return Console.Console.of({
|
|
82
|
-
[Console.TypeId]: Console.TypeId,
|
|
83
|
-
assert: (condition, ...args) => log("assert", condition, ...args),
|
|
84
|
-
clear: Effect.void,
|
|
85
|
-
count: (_label) => Effect.void,
|
|
86
|
-
countReset: (_label) => Effect.void,
|
|
87
|
-
debug: (...args) => log("debug", ...args),
|
|
88
|
-
dir: (item, options) => log("dir", item, options),
|
|
89
|
-
dirxml: (...args) => log("dirxml", ...args),
|
|
90
|
-
error: (...args) => log("error", ...args),
|
|
91
|
-
group: (...args) => log("group", ...args),
|
|
92
|
-
groupEnd: log("groupEnd"),
|
|
93
|
-
info: (...args) => log("info", ...args),
|
|
94
|
-
log: (...args) => log("log", ...args),
|
|
95
|
-
table: (tabularData, properties) => log("table", tabularData, properties),
|
|
96
|
-
time: (_label) => Effect.void,
|
|
97
|
-
timeEnd: (_label) => Effect.void,
|
|
98
|
-
timeLog: (_label, ...args) => log("info", ...args),
|
|
99
|
-
trace: (...args) => log("trace", ...args),
|
|
100
|
-
warn: (...args) => log("warn", ...args),
|
|
101
|
-
unsafe: {
|
|
102
|
-
assert(condition, ...args) {
|
|
103
|
-
unsafeLog("assert", condition, ...args);
|
|
104
|
-
},
|
|
105
|
-
clear() {},
|
|
106
|
-
count(_label) {},
|
|
107
|
-
countReset(_label) {},
|
|
108
|
-
debug(...args) {
|
|
109
|
-
unsafeLog("debug", ...args);
|
|
110
|
-
},
|
|
111
|
-
dir(item, options) {
|
|
112
|
-
unsafeLog("dir", item, options);
|
|
113
|
-
},
|
|
114
|
-
dirxml(...args) {
|
|
115
|
-
unsafeLog("dirxml", ...args);
|
|
116
|
-
},
|
|
117
|
-
error(...args) {
|
|
118
|
-
unsafeLog("error", ...args);
|
|
119
|
-
},
|
|
120
|
-
group(...args) {
|
|
121
|
-
unsafeLog("group", ...args);
|
|
122
|
-
},
|
|
123
|
-
groupCollapsed(...args) {
|
|
124
|
-
unsafeLog("groupCollapsed", ...args);
|
|
125
|
-
},
|
|
126
|
-
groupEnd() {
|
|
127
|
-
unsafeLog("groupEnd");
|
|
128
|
-
},
|
|
129
|
-
info(...args) {
|
|
130
|
-
unsafeLog("info", ...args);
|
|
131
|
-
},
|
|
132
|
-
log(...args) {
|
|
133
|
-
unsafeLog("log", ...args);
|
|
134
|
-
},
|
|
135
|
-
table(tabularData, properties) {
|
|
136
|
-
unsafeLog("table", tabularData, properties);
|
|
137
|
-
},
|
|
138
|
-
time(_label) {},
|
|
139
|
-
timeEnd(_label) {},
|
|
140
|
-
timeLog(_label, ...args) {
|
|
141
|
-
unsafeLog("info", ...args);
|
|
142
|
-
},
|
|
143
|
-
trace(...args) {
|
|
144
|
-
unsafeLog("trace", ...args);
|
|
145
|
-
},
|
|
146
|
-
warn(...args) {
|
|
147
|
-
unsafeLog("warn", ...args);
|
|
148
|
-
},
|
|
149
|
-
},
|
|
150
|
-
});
|
|
151
|
-
};
|
|
152
|
-
|
|
153
|
-
export const makeConsoleBridge = (
|
|
154
|
-
outerConsole: Console.Console,
|
|
155
|
-
pendingLogsRef: Ref.Ref<ReadonlyArray<BufferedConsoleCall>>,
|
|
156
|
-
markDirty: Effect.Effect<void, never, never>,
|
|
157
|
-
): ConsoleBridge => {
|
|
158
|
-
const appendLog = (call: BufferedConsoleCall) =>
|
|
159
|
-
call.args.length === 0
|
|
160
|
-
? Effect.void
|
|
161
|
-
: Ref.update(pendingLogsRef, (logs) => [...logs, call]).pipe(Effect.zipRight(markDirty));
|
|
162
|
-
|
|
163
|
-
const replayLogs = (logs: ReadonlyArray<BufferedConsoleCall>) =>
|
|
164
|
-
Effect.forEach(logs, (call) => replayBufferedConsoleCall(outerConsole, call), {
|
|
165
|
-
discard: true,
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
const log = (...args: ReadonlyArray<unknown>) =>
|
|
169
|
-
args.length === 0 ? Effect.void : appendLog({ method: "log", args, unsafe: false });
|
|
170
|
-
|
|
171
|
-
return {
|
|
172
|
-
progressConsole: makeProgressConsole(appendLog),
|
|
173
|
-
appendLog,
|
|
174
|
-
replayLogs,
|
|
175
|
-
log,
|
|
176
|
-
};
|
|
177
|
-
};
|