effective-progress 0.4.2 → 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 +16 -110
- package/package.json +11 -5
- 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 +13 -117
- package/src/types.ts +0 -73
- package/src/console.ts +0 -85
- package/src/renderer/ansi.ts +0 -138
- package/src/renderer/types.ts +0 -67
- package/src/renderer.ts +0 -1254
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { formatAmount, formatElapsed, formatEta } from "./format";
|
|
2
|
+
import { renderTreePrefix } from "./tree";
|
|
3
|
+
import type { TaskRowModel } from "./types";
|
|
4
|
+
|
|
5
|
+
export const DEFAULT_BAR_WIDTH = 20;
|
|
6
|
+
const MIN_DESCRIPTION_WIDTH = 8;
|
|
7
|
+
const MIN_BAR_WIDTH = 8;
|
|
8
|
+
const MIN_ELAPSED_WIDTH = 3;
|
|
9
|
+
const MIN_AMOUNT_WIDTH = 1;
|
|
10
|
+
const BASELINE_ROW_WIDTH = 100;
|
|
11
|
+
export const MIN_DESCRIPTION_COLUMNS_FOR_TREE = 24;
|
|
12
|
+
const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
|
|
13
|
+
const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
|
|
14
|
+
|
|
15
|
+
const textWidth = (text: string): number => Array.from(text).length;
|
|
16
|
+
|
|
17
|
+
export interface SharedColumnWidths {
|
|
18
|
+
readonly row: number;
|
|
19
|
+
readonly description: number;
|
|
20
|
+
readonly bar: number;
|
|
21
|
+
readonly amount: number;
|
|
22
|
+
readonly elapsed: number;
|
|
23
|
+
readonly eta: number;
|
|
24
|
+
readonly showTree: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const computeWidths = (
|
|
28
|
+
rows: ReadonlyArray<TaskRowModel>,
|
|
29
|
+
now: number,
|
|
30
|
+
tick: number,
|
|
31
|
+
terminalColumns?: number,
|
|
32
|
+
includeTree = true,
|
|
33
|
+
): Omit<SharedColumnWidths, "showTree"> => {
|
|
34
|
+
let hasDeterminate = false;
|
|
35
|
+
let description = MIN_DESCRIPTION_WIDTH;
|
|
36
|
+
let amount = 1;
|
|
37
|
+
let elapsed = RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR;
|
|
38
|
+
let eta = RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR;
|
|
39
|
+
|
|
40
|
+
for (const row of rows) {
|
|
41
|
+
const { task, tree } = row;
|
|
42
|
+
const treePrefix = includeTree ? renderTreePrefix(tree) : "";
|
|
43
|
+
description = Math.max(description, textWidth(`${treePrefix}${task.description}`));
|
|
44
|
+
|
|
45
|
+
if (task.units._tag === "DeterminateTaskUnits") {
|
|
46
|
+
hasDeterminate = true;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
amount = Math.max(amount, textWidth(formatAmount(task, tick)));
|
|
50
|
+
elapsed = Math.max(elapsed, textWidth(formatElapsed(task, now)));
|
|
51
|
+
|
|
52
|
+
if (task.status === "running" && task.units._tag === "DeterminateTaskUnits") {
|
|
53
|
+
const etaValue = formatEta(task, now);
|
|
54
|
+
const etaText = `ETA: ${etaValue.length > 0 ? etaValue : "--"}`;
|
|
55
|
+
eta = Math.max(eta, textWidth(etaText));
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const bar = hasDeterminate ? DEFAULT_BAR_WIDTH : 0;
|
|
60
|
+
let widths = {
|
|
61
|
+
description,
|
|
62
|
+
bar,
|
|
63
|
+
amount,
|
|
64
|
+
elapsed,
|
|
65
|
+
eta,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const visible = (w: typeof widths): Array<number> =>
|
|
69
|
+
[w.description, w.bar, w.amount, w.elapsed, w.eta].filter((width) => width > 0);
|
|
70
|
+
const total = (w: typeof widths): number => {
|
|
71
|
+
const cols = visible(w);
|
|
72
|
+
return cols.reduce((sum, width) => sum + width, 0) + Math.max(0, cols.length - 1);
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const baselineTarget = Math.max(BASELINE_ROW_WIDTH, total(widths));
|
|
76
|
+
const target =
|
|
77
|
+
terminalColumns === undefined
|
|
78
|
+
? baselineTarget
|
|
79
|
+
: Math.max(1, Math.min(Math.max(1, Math.floor(terminalColumns)), baselineTarget));
|
|
80
|
+
|
|
81
|
+
if (total(widths) < target) {
|
|
82
|
+
widths.description += target - total(widths);
|
|
83
|
+
} else if (total(widths) > target) {
|
|
84
|
+
let overflow = total(widths) - target;
|
|
85
|
+
|
|
86
|
+
const reduceBy = (key: keyof typeof widths, min: number) => {
|
|
87
|
+
if (overflow <= 0) {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const current = widths[key];
|
|
91
|
+
if (current <= min) {
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const reducible = current - min;
|
|
95
|
+
const delta = Math.min(reducible, overflow);
|
|
96
|
+
widths = { ...widths, [key]: current - delta };
|
|
97
|
+
overflow -= delta;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// Compress the description first, then optional columns.
|
|
101
|
+
reduceBy("description", MIN_DESCRIPTION_WIDTH);
|
|
102
|
+
reduceBy("eta", 0);
|
|
103
|
+
reduceBy("bar", MIN_BAR_WIDTH);
|
|
104
|
+
reduceBy("bar", 0);
|
|
105
|
+
reduceBy("elapsed", MIN_ELAPSED_WIDTH);
|
|
106
|
+
reduceBy("amount", MIN_AMOUNT_WIDTH);
|
|
107
|
+
reduceBy("description", 0);
|
|
108
|
+
|
|
109
|
+
if (total(widths) < target) {
|
|
110
|
+
widths.description += target - total(widths);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const rowWidth = total(widths);
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
row: rowWidth,
|
|
118
|
+
description: widths.description,
|
|
119
|
+
bar: widths.bar,
|
|
120
|
+
amount: widths.amount,
|
|
121
|
+
elapsed: widths.elapsed,
|
|
122
|
+
eta: widths.eta,
|
|
123
|
+
};
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export const computeSharedColumnWidths = (
|
|
127
|
+
rows: ReadonlyArray<TaskRowModel>,
|
|
128
|
+
now: number,
|
|
129
|
+
tick: number,
|
|
130
|
+
terminalColumns?: number,
|
|
131
|
+
): SharedColumnWidths => {
|
|
132
|
+
const withTree = computeWidths(rows, now, tick, terminalColumns, true);
|
|
133
|
+
if (withTree.description >= MIN_DESCRIPTION_COLUMNS_FOR_TREE) {
|
|
134
|
+
return {
|
|
135
|
+
...withTree,
|
|
136
|
+
showTree: true,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const withoutTree = computeWidths(rows, now, tick, terminalColumns, false);
|
|
141
|
+
return {
|
|
142
|
+
...withoutTree,
|
|
143
|
+
showTree: false,
|
|
144
|
+
};
|
|
145
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { TaskStore } from "../types";
|
|
2
|
+
import type { OrderedTask, TaskRowModel } from "./types";
|
|
3
|
+
import { computeTreeInfo } from "./tree";
|
|
4
|
+
|
|
5
|
+
const orderedVisibleTasks = (store: TaskStore): ReadonlyArray<OrderedTask> =>
|
|
6
|
+
store.renderOrder.flatMap((row) => {
|
|
7
|
+
const snapshot = store.tasks.get(row.id);
|
|
8
|
+
if (!snapshot || (snapshot.transient && snapshot.status !== "running")) {
|
|
9
|
+
return [];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
snapshot,
|
|
15
|
+
depth: row.depth,
|
|
16
|
+
},
|
|
17
|
+
];
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const toTaskRows = (store: TaskStore): ReadonlyArray<TaskRowModel> =>
|
|
21
|
+
computeTreeInfo(orderedVisibleTasks(store)).map((entry) => ({
|
|
22
|
+
task: entry.snapshot,
|
|
23
|
+
tree: entry.tree,
|
|
24
|
+
}));
|
|
@@ -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,48 +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 { formatWithOptions } from "node:util";
|
|
5
|
-
import type { PartialDeep } from "type-fest";
|
|
6
|
-
import { makeProgressConsole } from "./console";
|
|
7
|
-
import { Columns, FrameRenderer } from "./renderer";
|
|
3
|
+
import { InkRenderer } from "./ink-renderer";
|
|
8
4
|
import { ProgressTerminal } from "./terminal";
|
|
9
5
|
import type {
|
|
10
6
|
AddTaskOptions,
|
|
11
7
|
ProgressService,
|
|
12
8
|
RenderRow,
|
|
13
|
-
RendererConfigShape,
|
|
14
9
|
TaskStore,
|
|
15
10
|
UpdateTaskOptions,
|
|
16
11
|
} from "./types";
|
|
17
12
|
import {
|
|
18
|
-
decodeProgressBarConfigSync,
|
|
19
|
-
decodeRendererConfigSync,
|
|
20
|
-
defaultProgressBarConfig,
|
|
21
|
-
defaultRendererConfig,
|
|
22
13
|
DeterminateTaskUnits,
|
|
23
14
|
IndeterminateTaskUnits,
|
|
24
|
-
ProgressBarConfig,
|
|
25
|
-
RendererConfig,
|
|
26
15
|
Task,
|
|
27
16
|
TaskId,
|
|
28
17
|
TaskSnapshot,
|
|
29
18
|
} from "./types";
|
|
30
19
|
|
|
31
|
-
const mergeConfig = <T extends Record<PropertyKey, any>>(
|
|
32
|
-
base: T,
|
|
33
|
-
override: PartialDeep<T> | undefined,
|
|
34
|
-
): T =>
|
|
35
|
-
mergeWith(
|
|
36
|
-
structuredClone(base),
|
|
37
|
-
(override ?? {}) as Record<PropertyKey, any>,
|
|
38
|
-
(_targetValue, sourceValue) => {
|
|
39
|
-
if (Array.isArray(sourceValue)) {
|
|
40
|
-
return sourceValue;
|
|
41
|
-
}
|
|
42
|
-
return undefined;
|
|
43
|
-
},
|
|
44
|
-
) as T;
|
|
45
|
-
|
|
46
20
|
const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): TaskSnapshot => {
|
|
47
21
|
const currentUnits = snapshot.units;
|
|
48
22
|
const units = (() => {
|
|
@@ -82,7 +56,6 @@ const updatedSnapshot = (snapshot: TaskSnapshot, options: UpdateTaskOptions): Ta
|
|
|
82
56
|
status: snapshot.status,
|
|
83
57
|
transient: options.transient ?? snapshot.transient,
|
|
84
58
|
units,
|
|
85
|
-
config: snapshot.config,
|
|
86
59
|
startedAt: snapshot.startedAt,
|
|
87
60
|
completedAt: snapshot.completedAt,
|
|
88
61
|
});
|
|
@@ -96,7 +69,6 @@ const withTransient = (snapshot: TaskSnapshot, transient: boolean): TaskSnapshot
|
|
|
96
69
|
status: snapshot.status,
|
|
97
70
|
transient,
|
|
98
71
|
units: snapshot.units,
|
|
99
|
-
config: snapshot.config,
|
|
100
72
|
startedAt: snapshot.startedAt,
|
|
101
73
|
completedAt: snapshot.completedAt,
|
|
102
74
|
});
|
|
@@ -130,62 +102,28 @@ const removeFromRenderOrder = (
|
|
|
130
102
|
return next;
|
|
131
103
|
};
|
|
132
104
|
|
|
133
|
-
const withDefaultColumns = (rendererConfig: RendererConfigShape): RendererConfigShape => ({
|
|
134
|
-
...rendererConfig,
|
|
135
|
-
columns: rendererConfig.columns.length > 0 ? rendererConfig.columns : Columns.defaults(),
|
|
136
|
-
});
|
|
137
|
-
|
|
138
105
|
const makeProgressService = Effect.gen(function* () {
|
|
139
|
-
const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
|
|
140
|
-
const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
|
|
141
|
-
|
|
142
|
-
const rendererConfig = withDefaultColumns(
|
|
143
|
-
decodeRendererConfigSync(
|
|
144
|
-
mergeConfig(
|
|
145
|
-
defaultRendererConfig,
|
|
146
|
-
Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
|
|
147
|
-
),
|
|
148
|
-
),
|
|
149
|
-
);
|
|
150
|
-
|
|
151
|
-
const progressBarConfig = decodeProgressBarConfigSync(
|
|
152
|
-
mergeConfig(
|
|
153
|
-
defaultProgressBarConfig,
|
|
154
|
-
Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
|
|
155
|
-
),
|
|
156
|
-
);
|
|
157
|
-
|
|
158
106
|
const terminal = yield* ProgressTerminal;
|
|
159
|
-
const
|
|
107
|
+
const inkRenderer = yield* InkRenderer;
|
|
108
|
+
const outerConsole = yield* Effect.console;
|
|
160
109
|
const isTTY = yield* terminal.isTTY;
|
|
161
|
-
const maxRetainedLogLines = Math.max(0, Math.floor(rendererConfig.maxLogLines ?? 0));
|
|
162
110
|
|
|
163
111
|
const nextTaskIdRef = yield* Ref.make(0);
|
|
164
112
|
const storeRef = yield* Ref.make<TaskStore>({
|
|
165
113
|
tasks: new Map<TaskId, TaskSnapshot>(),
|
|
166
114
|
renderOrder: [],
|
|
167
115
|
});
|
|
168
|
-
const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
169
|
-
const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
|
|
170
116
|
const dirtyRef = yield* Ref.make(true);
|
|
171
117
|
const currentParentRef = yield* FiberRef.make(Option.none<TaskId>());
|
|
172
118
|
const scope = yield* Effect.scope;
|
|
173
119
|
|
|
174
|
-
yield* Effect.forkIn(
|
|
175
|
-
frameRenderer.run(
|
|
176
|
-
storeRef,
|
|
177
|
-
logsRef,
|
|
178
|
-
pendingLogsRef,
|
|
179
|
-
dirtyRef,
|
|
180
|
-
terminal,
|
|
181
|
-
isTTY,
|
|
182
|
-
rendererConfig,
|
|
183
|
-
maxRetainedLogLines,
|
|
184
|
-
),
|
|
185
|
-
scope,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
120
|
const markDirty = Ref.set(dirtyRef, true);
|
|
121
|
+
const log = (...args: ReadonlyArray<unknown>) =>
|
|
122
|
+
args.length === 0 ? Effect.void : outerConsole.log(...args);
|
|
123
|
+
|
|
124
|
+
yield* Effect.forkIn(inkRenderer.run(storeRef, dirtyRef, terminal, isTTY), scope);
|
|
125
|
+
// Let the renderer fiber start so queued logs are reliably flushed on scope teardown.
|
|
126
|
+
yield* Effect.sleep("0 millis");
|
|
189
127
|
|
|
190
128
|
const addTask = (options: AddTaskOptions) =>
|
|
191
129
|
Effect.gen(function* () {
|
|
@@ -202,10 +140,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
202
140
|
const parentSnapshot = Option.isSome(resolvedParentId)
|
|
203
141
|
? store.tasks.get(resolvedParentId.value)
|
|
204
142
|
: undefined;
|
|
205
|
-
const inheritedProgressBarConfig = parentSnapshot?.config ?? progressBarConfig;
|
|
206
|
-
const resolvedProgressBarConfig = decodeProgressBarConfigSync(
|
|
207
|
-
mergeConfig(inheritedProgressBarConfig, options.progressbar),
|
|
208
|
-
);
|
|
209
143
|
|
|
210
144
|
const now = yield* Clock.currentTimeMillis;
|
|
211
145
|
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
@@ -216,7 +150,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
216
150
|
status: "running",
|
|
217
151
|
transient: parentSnapshot?.transient ?? options.transient ?? false,
|
|
218
152
|
units,
|
|
219
|
-
config: resolvedProgressBarConfig,
|
|
220
153
|
startedAt: now,
|
|
221
154
|
completedAt: null,
|
|
222
155
|
});
|
|
@@ -292,7 +225,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
292
225
|
status: snapshot.status,
|
|
293
226
|
transient: snapshot.transient,
|
|
294
227
|
units,
|
|
295
|
-
config: snapshot.config,
|
|
296
228
|
startedAt: snapshot.startedAt,
|
|
297
229
|
completedAt: snapshot.completedAt,
|
|
298
230
|
}),
|
|
@@ -332,7 +264,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
332
264
|
total: snapshot.units.total,
|
|
333
265
|
})
|
|
334
266
|
: snapshot.units,
|
|
335
|
-
config: snapshot.config,
|
|
336
267
|
startedAt: snapshot.startedAt,
|
|
337
268
|
completedAt: now,
|
|
338
269
|
}),
|
|
@@ -367,7 +298,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
367
298
|
status: "failed",
|
|
368
299
|
transient: snapshot.transient,
|
|
369
300
|
units: snapshot.units,
|
|
370
|
-
config: snapshot.config,
|
|
371
301
|
startedAt: snapshot.startedAt,
|
|
372
302
|
completedAt: now,
|
|
373
303
|
}),
|
|
@@ -377,36 +307,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
377
307
|
yield* markDirty;
|
|
378
308
|
});
|
|
379
309
|
|
|
380
|
-
const appendLog = (args: ReadonlyArray<unknown>) =>
|
|
381
|
-
Effect.gen(function* () {
|
|
382
|
-
if (args.length === 0) {
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
const message = formatWithOptions(
|
|
387
|
-
{
|
|
388
|
-
colors: isTTY,
|
|
389
|
-
depth: 6,
|
|
390
|
-
},
|
|
391
|
-
...args,
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
yield* Ref.update(pendingLogsRef, (logs) => [...logs, message]);
|
|
395
|
-
if (maxRetainedLogLines > 0) {
|
|
396
|
-
yield* Ref.update(logsRef, (logs) => {
|
|
397
|
-
const next = [...logs, message];
|
|
398
|
-
if (next.length <= maxRetainedLogLines) {
|
|
399
|
-
return next;
|
|
400
|
-
}
|
|
401
|
-
return next.slice(next.length - maxRetainedLogLines);
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
yield* markDirty;
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
const log = (...args: ReadonlyArray<unknown>) => appendLog(args);
|
|
409
|
-
|
|
410
310
|
const getTask = (taskId: TaskId) =>
|
|
411
311
|
Ref.get(storeRef).pipe(Effect.map((store) => Option.fromNullable(store.tasks.get(taskId))));
|
|
412
312
|
|
|
@@ -416,7 +316,6 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
416
316
|
2,
|
|
417
317
|
<A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
|
|
418
318
|
Effect.gen(function* () {
|
|
419
|
-
const outerConsole = yield* Effect.console;
|
|
420
319
|
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
421
320
|
const resolvedParentId =
|
|
422
321
|
options.parentId === undefined ? inheritedParentId : Option.some(options.parentId);
|
|
@@ -428,10 +327,7 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
428
327
|
});
|
|
429
328
|
|
|
430
329
|
return yield* Effect.locally(
|
|
431
|
-
Effect.
|
|
432
|
-
Effect.provideService(effect, Task, taskId),
|
|
433
|
-
makeProgressConsole(log, outerConsole),
|
|
434
|
-
),
|
|
330
|
+
Effect.provideService(effect, Task, taskId),
|
|
435
331
|
currentParentRef,
|
|
436
332
|
Option.some(taskId),
|
|
437
333
|
);
|
|
@@ -484,11 +380,11 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
|
|
|
484
380
|
static readonly Default = Layer.unwrapEffect(
|
|
485
381
|
Effect.gen(function* () {
|
|
486
382
|
const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
|
|
487
|
-
const
|
|
383
|
+
const inkRendererOption = yield* Effect.serviceOption(InkRenderer);
|
|
488
384
|
let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
|
|
489
385
|
|
|
490
|
-
if (Option.isNone(
|
|
491
|
-
layer = layer.pipe(Layer.provide(
|
|
386
|
+
if (Option.isNone(inkRendererOption)) {
|
|
387
|
+
layer = layer.pipe(Layer.provide(InkRenderer.Default));
|
|
492
388
|
}
|
|
493
389
|
if (Option.isNone(terminalOption)) {
|
|
494
390
|
layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
|