effective-progress 0.5.4 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +11 -19
- package/dist/index.d.mts +4 -6
- package/dist/index.mjs +1175 -620
- package/package.json +5 -4
package/dist/index.mjs
CHANGED
|
@@ -1,8 +1,454 @@
|
|
|
1
|
-
import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option,
|
|
1
|
+
import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
|
|
2
2
|
import { dual } from "effect/Function";
|
|
3
3
|
import { Box, Text, render } from "ink";
|
|
4
|
+
import { useEffect, useState, useSyncExternalStore } from "react";
|
|
5
|
+
import stringWidth from "fast-string-width";
|
|
4
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
7
|
|
|
8
|
+
//#region src/types.ts
|
|
9
|
+
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
10
|
+
const TaskId = Brand.nominal();
|
|
11
|
+
const TaskStatusSchema = Schema.Literal("running", "done", "failed");
|
|
12
|
+
const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
|
|
13
|
+
var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
|
|
14
|
+
succeeded: Schema.Number,
|
|
15
|
+
failed: Schema.Number,
|
|
16
|
+
processed: Schema.Number,
|
|
17
|
+
total: Schema.Number
|
|
18
|
+
}) {};
|
|
19
|
+
var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", {}) {};
|
|
20
|
+
const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
|
|
21
|
+
var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
|
|
22
|
+
id: TaskIdSchema,
|
|
23
|
+
parentId: Schema.NullOr(TaskIdSchema),
|
|
24
|
+
description: Schema.String,
|
|
25
|
+
status: TaskStatusSchema,
|
|
26
|
+
countDisplay: TaskCountDisplaySchema,
|
|
27
|
+
transient: Schema.Boolean,
|
|
28
|
+
units: TaskUnitsSchema,
|
|
29
|
+
startedAt: Schema.Number,
|
|
30
|
+
completedAt: Schema.NullOr(Schema.Number)
|
|
31
|
+
}) {};
|
|
32
|
+
var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
|
|
33
|
+
var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
|
|
34
|
+
taskId: TaskIdSchema,
|
|
35
|
+
parentId: Schema.NullOr(TaskIdSchema),
|
|
36
|
+
description: Schema.String,
|
|
37
|
+
total: Schema.optional(Schema.Number),
|
|
38
|
+
transient: Schema.Boolean,
|
|
39
|
+
countDisplay: TaskCountDisplaySchema
|
|
40
|
+
}) {};
|
|
41
|
+
var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
|
|
42
|
+
taskId: TaskIdSchema,
|
|
43
|
+
description: Schema.optional(Schema.String),
|
|
44
|
+
succeeded: Schema.optional(Schema.Number),
|
|
45
|
+
failed: Schema.optional(Schema.Number),
|
|
46
|
+
processed: Schema.optional(Schema.Number),
|
|
47
|
+
total: Schema.optional(Schema.Number),
|
|
48
|
+
transient: Schema.optional(Schema.Boolean),
|
|
49
|
+
countDisplay: Schema.optional(TaskCountDisplaySchema)
|
|
50
|
+
}) {};
|
|
51
|
+
var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
|
|
52
|
+
taskId: TaskIdSchema,
|
|
53
|
+
amount: Schema.Number,
|
|
54
|
+
kind: Schema.Literal("succeeded", "failed")
|
|
55
|
+
}) {};
|
|
56
|
+
var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
|
|
57
|
+
var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
|
|
58
|
+
var TaskRemovedEvent = class extends Schema.TaggedClass()("TaskRemoved", { taskId: TaskIdSchema }) {};
|
|
59
|
+
const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskRemovedEvent);
|
|
60
|
+
const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region src/ink-renderer/snapshot/render-snapshot.ts
|
|
64
|
+
const orderedVisibleTasks = (store) => store.renderOrder.flatMap((row) => {
|
|
65
|
+
const snapshot = store.tasks.get(row.id);
|
|
66
|
+
if (!snapshot || snapshot.transient && snapshot.status !== "running") return [];
|
|
67
|
+
return [{
|
|
68
|
+
snapshot,
|
|
69
|
+
depth: row.depth
|
|
70
|
+
}];
|
|
71
|
+
});
|
|
72
|
+
const computeTreeInfo = (ordered) => {
|
|
73
|
+
const hasNextSiblingByIndex = Array.from({ length: ordered.length }, () => false);
|
|
74
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
75
|
+
const depth = ordered[i].depth;
|
|
76
|
+
for (let j = i + 1; j < ordered.length; j++) {
|
|
77
|
+
const candidateDepth = ordered[j].depth;
|
|
78
|
+
if (candidateDepth < depth) break;
|
|
79
|
+
if (candidateDepth === depth) {
|
|
80
|
+
hasNextSiblingByIndex[i] = true;
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const ancestorStateByDepth = [];
|
|
86
|
+
return ordered.map((entry, index) => {
|
|
87
|
+
const depth = entry.depth;
|
|
88
|
+
ancestorStateByDepth.length = depth;
|
|
89
|
+
const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
|
|
90
|
+
const tree = {
|
|
91
|
+
depth,
|
|
92
|
+
hasNextSibling: hasNextSiblingByIndex[index] ?? false,
|
|
93
|
+
hasChildren,
|
|
94
|
+
ancestorHasNextSibling: [...ancestorStateByDepth]
|
|
95
|
+
};
|
|
96
|
+
ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
|
|
97
|
+
return {
|
|
98
|
+
...entry,
|
|
99
|
+
tree
|
|
100
|
+
};
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
const toRenderSnapshot = (store) => {
|
|
104
|
+
const visibleTasks = orderedVisibleTasks(store);
|
|
105
|
+
const hasRunningTasks = visibleTasks.some((entry) => entry.snapshot.status === "running");
|
|
106
|
+
return {
|
|
107
|
+
rows: computeTreeInfo(visibleTasks).map((entry) => ({
|
|
108
|
+
task: entry.snapshot,
|
|
109
|
+
tree: entry.tree
|
|
110
|
+
})),
|
|
111
|
+
hasRunningTasks
|
|
112
|
+
};
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
//#endregion
|
|
116
|
+
//#region src/ink-renderer/store.ts
|
|
117
|
+
const normalizeDeterminateCounts = (counts) => {
|
|
118
|
+
const total = Math.max(0, counts.total);
|
|
119
|
+
const failed = Math.min(total, Math.max(0, counts.failed));
|
|
120
|
+
const succeeded = Math.min(total - failed, Math.max(0, counts.succeeded));
|
|
121
|
+
return new DeterminateTaskUnits({
|
|
122
|
+
succeeded,
|
|
123
|
+
failed,
|
|
124
|
+
processed: succeeded + failed,
|
|
125
|
+
total
|
|
126
|
+
});
|
|
127
|
+
};
|
|
128
|
+
const updateDeterminateCounts = (units, options) => normalizeDeterminateCounts({
|
|
129
|
+
succeeded: options.succeeded ?? units.succeeded,
|
|
130
|
+
failed: options.failed ?? units.failed,
|
|
131
|
+
total: options.total ?? units.total
|
|
132
|
+
});
|
|
133
|
+
const updatedSnapshot = (snapshot, options) => {
|
|
134
|
+
const currentUnits = snapshot.units;
|
|
135
|
+
const units = (() => {
|
|
136
|
+
if (options.total !== void 0) {
|
|
137
|
+
if (options.total <= 0) return new IndeterminateTaskUnits({});
|
|
138
|
+
if (currentUnits._tag === "DeterminateTaskUnits") return updateDeterminateCounts(currentUnits, options);
|
|
139
|
+
return normalizeDeterminateCounts({
|
|
140
|
+
succeeded: options.succeeded ?? 0,
|
|
141
|
+
failed: options.failed ?? 0,
|
|
142
|
+
total: options.total
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
if (currentUnits._tag === "DeterminateTaskUnits") {
|
|
146
|
+
if (options.succeeded === void 0 && options.failed === void 0) return currentUnits;
|
|
147
|
+
return updateDeterminateCounts(currentUnits, options);
|
|
148
|
+
}
|
|
149
|
+
return currentUnits;
|
|
150
|
+
})();
|
|
151
|
+
return new TaskSnapshot({
|
|
152
|
+
id: snapshot.id,
|
|
153
|
+
parentId: snapshot.parentId,
|
|
154
|
+
description: options.description ?? snapshot.description,
|
|
155
|
+
status: snapshot.status,
|
|
156
|
+
countDisplay: options.countDisplay ?? snapshot.countDisplay,
|
|
157
|
+
transient: options.transient ?? snapshot.transient,
|
|
158
|
+
units,
|
|
159
|
+
startedAt: snapshot.startedAt,
|
|
160
|
+
completedAt: snapshot.completedAt
|
|
161
|
+
});
|
|
162
|
+
};
|
|
163
|
+
const withTransient = (snapshot, transient) => new TaskSnapshot({
|
|
164
|
+
id: snapshot.id,
|
|
165
|
+
parentId: snapshot.parentId,
|
|
166
|
+
description: snapshot.description,
|
|
167
|
+
status: snapshot.status,
|
|
168
|
+
countDisplay: snapshot.countDisplay,
|
|
169
|
+
transient,
|
|
170
|
+
units: snapshot.units,
|
|
171
|
+
startedAt: snapshot.startedAt,
|
|
172
|
+
completedAt: snapshot.completedAt
|
|
173
|
+
});
|
|
174
|
+
const findInsertionIndex = (renderOrder, parentId) => {
|
|
175
|
+
if (parentId === null) return {
|
|
176
|
+
index: renderOrder.length,
|
|
177
|
+
depth: 0
|
|
178
|
+
};
|
|
179
|
+
const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
|
|
180
|
+
if (parentIdx === -1) return {
|
|
181
|
+
index: renderOrder.length,
|
|
182
|
+
depth: 0
|
|
183
|
+
};
|
|
184
|
+
const parentDepth = renderOrder[parentIdx].depth;
|
|
185
|
+
let i = parentIdx + 1;
|
|
186
|
+
while (i < renderOrder.length && renderOrder[i].depth > parentDepth) i++;
|
|
187
|
+
return {
|
|
188
|
+
index: i,
|
|
189
|
+
depth: parentDepth + 1
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
const removeFromRenderOrder = (renderOrder, taskId) => {
|
|
193
|
+
const idx = renderOrder.findIndex((row) => row.id === taskId);
|
|
194
|
+
if (idx === -1) return renderOrder;
|
|
195
|
+
const taskDepth = renderOrder[idx].depth;
|
|
196
|
+
let end = idx + 1;
|
|
197
|
+
while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
|
|
198
|
+
const next = [...renderOrder];
|
|
199
|
+
next.splice(idx, end - idx);
|
|
200
|
+
return next;
|
|
201
|
+
};
|
|
202
|
+
const SNAPSHOT_PUBLISH_INTERVAL_MILLIS = 50;
|
|
203
|
+
const makeProgressRenderStore = () => {
|
|
204
|
+
let nextTaskId = 0;
|
|
205
|
+
let state = {
|
|
206
|
+
tasks: /* @__PURE__ */ new Map(),
|
|
207
|
+
renderOrder: []
|
|
208
|
+
};
|
|
209
|
+
let publishedSnapshot = toRenderSnapshot(state);
|
|
210
|
+
let hasPendingPublish = false;
|
|
211
|
+
let lastPublishAt = 0;
|
|
212
|
+
let publishTimeout;
|
|
213
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
214
|
+
const notifyListeners = () => {
|
|
215
|
+
for (const listener of listeners) listener();
|
|
216
|
+
};
|
|
217
|
+
const publishNow = () => {
|
|
218
|
+
hasPendingPublish = false;
|
|
219
|
+
lastPublishAt = Date.now();
|
|
220
|
+
publishedSnapshot = toRenderSnapshot(state);
|
|
221
|
+
notifyListeners();
|
|
222
|
+
};
|
|
223
|
+
const clearScheduledPublish = () => {
|
|
224
|
+
if (publishTimeout === void 0) return;
|
|
225
|
+
clearTimeout(publishTimeout);
|
|
226
|
+
publishTimeout = void 0;
|
|
227
|
+
};
|
|
228
|
+
const schedulePublish = () => {
|
|
229
|
+
if (!hasPendingPublish) return;
|
|
230
|
+
const now = Date.now();
|
|
231
|
+
const waitMillis = Math.max(0, SNAPSHOT_PUBLISH_INTERVAL_MILLIS - (now - lastPublishAt));
|
|
232
|
+
if (waitMillis === 0) {
|
|
233
|
+
clearScheduledPublish();
|
|
234
|
+
publishNow();
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (publishTimeout !== void 0) return;
|
|
238
|
+
publishTimeout = setTimeout(() => {
|
|
239
|
+
publishTimeout = void 0;
|
|
240
|
+
if (!hasPendingPublish) return;
|
|
241
|
+
publishNow();
|
|
242
|
+
}, waitMillis);
|
|
243
|
+
};
|
|
244
|
+
const publish = (nextState) => {
|
|
245
|
+
if (nextState === state) return;
|
|
246
|
+
state = nextState;
|
|
247
|
+
hasPendingPublish = true;
|
|
248
|
+
schedulePublish();
|
|
249
|
+
};
|
|
250
|
+
const updateState = (transform) => {
|
|
251
|
+
publish(transform(state));
|
|
252
|
+
};
|
|
253
|
+
return {
|
|
254
|
+
getSnapshot: () => publishedSnapshot,
|
|
255
|
+
subscribe: (listener) => {
|
|
256
|
+
listeners.add(listener);
|
|
257
|
+
return () => {
|
|
258
|
+
listeners.delete(listener);
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
flush: () => {
|
|
262
|
+
if (!hasPendingPublish) return;
|
|
263
|
+
clearScheduledPublish();
|
|
264
|
+
publishNow();
|
|
265
|
+
},
|
|
266
|
+
addTask: (options) => Effect.gen(function* () {
|
|
267
|
+
const taskId = TaskId(++nextTaskId);
|
|
268
|
+
const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({}) : normalizeDeterminateCounts({
|
|
269
|
+
succeeded: 0,
|
|
270
|
+
failed: 0,
|
|
271
|
+
total: options.total
|
|
272
|
+
});
|
|
273
|
+
const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
|
|
274
|
+
const now = yield* Clock.currentTimeMillis;
|
|
275
|
+
const parentId = options.parentId ?? null;
|
|
276
|
+
const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
|
|
277
|
+
const task = new TaskSnapshot({
|
|
278
|
+
id: taskId,
|
|
279
|
+
parentId,
|
|
280
|
+
description: options.description,
|
|
281
|
+
status: "running",
|
|
282
|
+
countDisplay,
|
|
283
|
+
transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
|
|
284
|
+
units,
|
|
285
|
+
startedAt: now,
|
|
286
|
+
completedAt: null
|
|
287
|
+
});
|
|
288
|
+
updateState((current) => {
|
|
289
|
+
const nextTasks = new Map(current.tasks);
|
|
290
|
+
nextTasks.set(taskId, task);
|
|
291
|
+
const { index, depth } = findInsertionIndex(current.renderOrder, parentId);
|
|
292
|
+
const nextRenderOrder = [...current.renderOrder];
|
|
293
|
+
nextRenderOrder.splice(index, 0, {
|
|
294
|
+
id: taskId,
|
|
295
|
+
depth
|
|
296
|
+
});
|
|
297
|
+
return {
|
|
298
|
+
tasks: nextTasks,
|
|
299
|
+
renderOrder: nextRenderOrder
|
|
300
|
+
};
|
|
301
|
+
});
|
|
302
|
+
return taskId;
|
|
303
|
+
}),
|
|
304
|
+
updateTask: (taskId, options) => Effect.sync(() => {
|
|
305
|
+
updateState((current) => {
|
|
306
|
+
const currentTask = current.tasks.get(taskId);
|
|
307
|
+
if (!currentTask) return current;
|
|
308
|
+
const nextTasks = new Map(current.tasks);
|
|
309
|
+
const nextTask = updatedSnapshot(currentTask, options);
|
|
310
|
+
nextTasks.set(taskId, nextTask);
|
|
311
|
+
if (options.transient !== void 0) for (const [candidateId, candidate] of current.tasks.entries()) {
|
|
312
|
+
if (candidateId === taskId) continue;
|
|
313
|
+
let parentId = candidate.parentId;
|
|
314
|
+
let isDescendant = false;
|
|
315
|
+
while (parentId !== null) {
|
|
316
|
+
if (parentId === taskId) {
|
|
317
|
+
isDescendant = true;
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
parentId = current.tasks.get(parentId)?.parentId ?? null;
|
|
321
|
+
}
|
|
322
|
+
if (isDescendant) nextTasks.set(candidateId, withTransient(candidate, nextTask.transient));
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
tasks: nextTasks,
|
|
326
|
+
renderOrder: current.renderOrder
|
|
327
|
+
};
|
|
328
|
+
});
|
|
329
|
+
}),
|
|
330
|
+
incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
|
|
331
|
+
updateState((current) => {
|
|
332
|
+
const currentTask = current.tasks.get(taskId);
|
|
333
|
+
if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
|
|
334
|
+
const nextTasks = new Map(current.tasks);
|
|
335
|
+
nextTasks.set(taskId, new TaskSnapshot({
|
|
336
|
+
id: currentTask.id,
|
|
337
|
+
parentId: currentTask.parentId,
|
|
338
|
+
description: currentTask.description,
|
|
339
|
+
status: currentTask.status,
|
|
340
|
+
countDisplay: currentTask.countDisplay,
|
|
341
|
+
transient: currentTask.transient,
|
|
342
|
+
units: normalizeDeterminateCounts({
|
|
343
|
+
succeeded: currentTask.units.succeeded + amount,
|
|
344
|
+
failed: currentTask.units.failed,
|
|
345
|
+
total: currentTask.units.total
|
|
346
|
+
}),
|
|
347
|
+
startedAt: currentTask.startedAt,
|
|
348
|
+
completedAt: currentTask.completedAt
|
|
349
|
+
}));
|
|
350
|
+
return {
|
|
351
|
+
tasks: nextTasks,
|
|
352
|
+
renderOrder: current.renderOrder
|
|
353
|
+
};
|
|
354
|
+
});
|
|
355
|
+
}),
|
|
356
|
+
incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
|
|
357
|
+
updateState((current) => {
|
|
358
|
+
const currentTask = current.tasks.get(taskId);
|
|
359
|
+
if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
|
|
360
|
+
const nextTasks = new Map(current.tasks);
|
|
361
|
+
nextTasks.set(taskId, new TaskSnapshot({
|
|
362
|
+
id: currentTask.id,
|
|
363
|
+
parentId: currentTask.parentId,
|
|
364
|
+
description: currentTask.description,
|
|
365
|
+
status: currentTask.status,
|
|
366
|
+
countDisplay: currentTask.countDisplay,
|
|
367
|
+
transient: currentTask.transient,
|
|
368
|
+
units: normalizeDeterminateCounts({
|
|
369
|
+
succeeded: currentTask.units.succeeded,
|
|
370
|
+
failed: currentTask.units.failed + amount,
|
|
371
|
+
total: currentTask.units.total
|
|
372
|
+
}),
|
|
373
|
+
startedAt: currentTask.startedAt,
|
|
374
|
+
completedAt: currentTask.completedAt
|
|
375
|
+
}));
|
|
376
|
+
return {
|
|
377
|
+
tasks: nextTasks,
|
|
378
|
+
renderOrder: current.renderOrder
|
|
379
|
+
};
|
|
380
|
+
});
|
|
381
|
+
}),
|
|
382
|
+
completeTask: (taskId) => Effect.gen(function* () {
|
|
383
|
+
const now = yield* Clock.currentTimeMillis;
|
|
384
|
+
updateState((current) => {
|
|
385
|
+
const currentTask = current.tasks.get(taskId);
|
|
386
|
+
if (!currentTask) return current;
|
|
387
|
+
const nextTasks = new Map(current.tasks);
|
|
388
|
+
if (currentTask.transient) {
|
|
389
|
+
nextTasks.delete(taskId);
|
|
390
|
+
return {
|
|
391
|
+
tasks: nextTasks,
|
|
392
|
+
renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
nextTasks.set(taskId, new TaskSnapshot({
|
|
396
|
+
id: currentTask.id,
|
|
397
|
+
parentId: currentTask.parentId,
|
|
398
|
+
description: currentTask.description,
|
|
399
|
+
status: "done",
|
|
400
|
+
countDisplay: currentTask.countDisplay,
|
|
401
|
+
transient: currentTask.transient,
|
|
402
|
+
units: currentTask.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
|
|
403
|
+
succeeded: currentTask.units.total - currentTask.units.failed,
|
|
404
|
+
failed: currentTask.units.failed,
|
|
405
|
+
total: currentTask.units.total
|
|
406
|
+
}) : currentTask.units,
|
|
407
|
+
startedAt: currentTask.startedAt,
|
|
408
|
+
completedAt: now
|
|
409
|
+
}));
|
|
410
|
+
return {
|
|
411
|
+
tasks: nextTasks,
|
|
412
|
+
renderOrder: current.renderOrder
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
}),
|
|
416
|
+
failTask: (taskId) => Effect.gen(function* () {
|
|
417
|
+
const now = yield* Clock.currentTimeMillis;
|
|
418
|
+
updateState((current) => {
|
|
419
|
+
const currentTask = current.tasks.get(taskId);
|
|
420
|
+
if (!currentTask) return current;
|
|
421
|
+
const nextTasks = new Map(current.tasks);
|
|
422
|
+
if (currentTask.transient) {
|
|
423
|
+
nextTasks.delete(taskId);
|
|
424
|
+
return {
|
|
425
|
+
tasks: nextTasks,
|
|
426
|
+
renderOrder: removeFromRenderOrder(current.renderOrder, taskId)
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
nextTasks.set(taskId, new TaskSnapshot({
|
|
430
|
+
id: currentTask.id,
|
|
431
|
+
parentId: currentTask.parentId,
|
|
432
|
+
description: currentTask.description,
|
|
433
|
+
status: "failed",
|
|
434
|
+
countDisplay: currentTask.countDisplay,
|
|
435
|
+
transient: currentTask.transient,
|
|
436
|
+
units: currentTask.units,
|
|
437
|
+
startedAt: currentTask.startedAt,
|
|
438
|
+
completedAt: now
|
|
439
|
+
}));
|
|
440
|
+
return {
|
|
441
|
+
tasks: nextTasks,
|
|
442
|
+
renderOrder: current.renderOrder
|
|
443
|
+
};
|
|
444
|
+
});
|
|
445
|
+
}),
|
|
446
|
+
getTask: (taskId) => Effect.sync(() => Option.fromNullable(state.tasks.get(taskId))),
|
|
447
|
+
listTasks: Effect.sync(() => Array.from(state.tasks.values()))
|
|
448
|
+
};
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
//#endregion
|
|
6
452
|
//#region src/ink-renderer/format.ts
|
|
7
453
|
const SPINNER_FRAMES = [
|
|
8
454
|
"⠋",
|
|
@@ -94,167 +540,38 @@ const formatAmount = (task, _tick) => {
|
|
|
94
540
|
};
|
|
95
541
|
|
|
96
542
|
//#endregion
|
|
97
|
-
//#region src/ink-renderer/
|
|
98
|
-
const
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
break;
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
const ancestorStateByDepth = [];
|
|
117
|
-
return ordered.map((entry, index) => {
|
|
118
|
-
const depth = entry.depth;
|
|
119
|
-
ancestorStateByDepth.length = depth;
|
|
120
|
-
const hasChildren = index + 1 < ordered.length && ordered[index + 1] !== void 0 && ordered[index + 1].depth > depth;
|
|
121
|
-
const tree = {
|
|
122
|
-
depth,
|
|
123
|
-
hasNextSibling: hasNextSiblingByIndex[index] ?? false,
|
|
124
|
-
hasChildren,
|
|
125
|
-
ancestorHasNextSibling: [...ancestorStateByDepth]
|
|
126
|
-
};
|
|
127
|
-
ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
|
|
128
|
-
return {
|
|
129
|
-
...entry,
|
|
130
|
-
tree
|
|
131
|
-
};
|
|
132
|
-
});
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
//#endregion
|
|
136
|
-
//#region src/ink-renderer/layout.ts
|
|
137
|
-
const DEFAULT_BAR_WIDTH = 30;
|
|
138
|
-
const MIN_DESCRIPTION_WIDTH = 8;
|
|
139
|
-
const MIN_BAR_WIDTH = 8;
|
|
140
|
-
const MIN_ELAPSED_WIDTH = 2;
|
|
141
|
-
const MIN_AMOUNT_WIDTH = 0;
|
|
142
|
-
const BASELINE_ROW_WIDTH = 100;
|
|
143
|
-
const MIN_DESCRIPTION_COLUMNS_FOR_TREE = 24;
|
|
144
|
-
const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
|
|
145
|
-
const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
|
|
146
|
-
const textWidth = (text) => Array.from(text).length;
|
|
147
|
-
const computeWidths = (rows, now, tick, terminalColumns, includeTree = true) => {
|
|
148
|
-
let hasDeterminate = false;
|
|
149
|
-
let hasDetailedDeterminate = false;
|
|
150
|
-
let description = MIN_DESCRIPTION_WIDTH;
|
|
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;
|
|
158
|
-
for (const row of rows) {
|
|
159
|
-
const { task, tree } = row;
|
|
160
|
-
const treePrefix = includeTree ? renderTreePrefix(tree) : "";
|
|
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)));
|
|
174
|
-
if (task.status === "running" && task.units._tag === "DeterminateTaskUnits") {
|
|
175
|
-
const etaValue = formatEta(task, now);
|
|
176
|
-
const etaText = `ETA: ${etaValue.length > 0 ? etaValue : "--"}`;
|
|
177
|
-
etaContentWidth = Math.max(etaContentWidth, textWidth(etaText));
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
const structuredAmount = hasDeterminate ? amountProcessed + 1 + amountTotal + (hasDetailedDeterminate ? amountSucceeded + 1 + amountFailed + 1 : 0) : 0;
|
|
181
|
-
if (hasDeterminate) amount = structuredAmount;
|
|
182
|
-
let widths = {
|
|
183
|
-
description,
|
|
184
|
-
bar: hasDeterminate ? DEFAULT_BAR_WIDTH : 0,
|
|
185
|
-
amount,
|
|
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
|
|
192
|
-
};
|
|
193
|
-
const visible = (w) => [
|
|
194
|
-
w.description,
|
|
195
|
-
w.bar,
|
|
196
|
-
w.amount,
|
|
197
|
-
w.elapsed,
|
|
198
|
-
w.eta
|
|
199
|
-
].filter((width) => width > 0);
|
|
200
|
-
const total = (w) => {
|
|
201
|
-
const cols = visible(w);
|
|
202
|
-
return cols.reduce((sum, width) => sum + width, 0) + Math.max(0, cols.length - 1);
|
|
203
|
-
};
|
|
204
|
-
const baselineTarget = hasDeterminate ? Math.max(BASELINE_ROW_WIDTH, total(widths)) : total(widths);
|
|
205
|
-
const target = terminalColumns === void 0 ? baselineTarget : Math.max(1, Math.min(Math.max(1, Math.floor(terminalColumns)), baselineTarget));
|
|
206
|
-
if (total(widths) < target) widths.description += target - total(widths);
|
|
207
|
-
else if (total(widths) > target) {
|
|
208
|
-
let overflow = total(widths) - target;
|
|
209
|
-
const reduceBy = (key, min) => {
|
|
210
|
-
if (overflow <= 0) return;
|
|
211
|
-
const current = widths[key];
|
|
212
|
-
if (current <= min) return;
|
|
213
|
-
const reducible = current - min;
|
|
214
|
-
const delta = Math.min(reducible, overflow);
|
|
215
|
-
widths = {
|
|
216
|
-
...widths,
|
|
217
|
-
[key]: current - delta
|
|
218
|
-
};
|
|
219
|
-
overflow -= delta;
|
|
220
|
-
};
|
|
221
|
-
reduceBy("eta", etaContentWidth);
|
|
222
|
-
reduceBy("elapsed", elapsedContentWidth);
|
|
223
|
-
reduceBy("bar", MIN_BAR_WIDTH);
|
|
224
|
-
reduceBy("eta", 0);
|
|
225
|
-
reduceBy("bar", 0);
|
|
226
|
-
reduceBy("elapsed", MIN_ELAPSED_WIDTH);
|
|
227
|
-
reduceBy("description", MIN_DESCRIPTION_WIDTH);
|
|
228
|
-
reduceBy("amount", MIN_AMOUNT_WIDTH);
|
|
229
|
-
reduceBy("description", 0);
|
|
230
|
-
if (total(widths) < target) widths.description += target - total(widths);
|
|
231
|
-
}
|
|
232
|
-
const rowWidth = total(widths);
|
|
233
|
-
const useStructuredAmount = hasDeterminate && widths.amount >= structuredAmount;
|
|
234
|
-
return {
|
|
235
|
-
row: rowWidth,
|
|
236
|
-
description: widths.description,
|
|
237
|
-
bar: widths.bar,
|
|
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,
|
|
243
|
-
elapsed: widths.elapsed,
|
|
244
|
-
eta: widths.eta
|
|
245
|
-
};
|
|
246
|
-
};
|
|
247
|
-
const computeSharedColumnWidths = (rows, now, tick, terminalColumns) => {
|
|
248
|
-
const withTree = computeWidths(rows, now, tick, terminalColumns, true);
|
|
249
|
-
if (withTree.description >= MIN_DESCRIPTION_COLUMNS_FOR_TREE) return {
|
|
250
|
-
...withTree,
|
|
251
|
-
showTree: true
|
|
252
|
-
};
|
|
253
|
-
return {
|
|
254
|
-
...computeWidths(rows, now, tick, terminalColumns, false),
|
|
255
|
-
showTree: false
|
|
256
|
-
};
|
|
543
|
+
//#region src/ink-renderer/columns/determinate.ts
|
|
544
|
+
const isDeterminate = (task) => task.units._tag === "DeterminateTaskUnits";
|
|
545
|
+
const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
|
|
546
|
+
|
|
547
|
+
//#endregion
|
|
548
|
+
//#region src/ink-renderer/columns/spec.ts
|
|
549
|
+
const WIDTH_CACHE_LIMIT = 4096;
|
|
550
|
+
const widthCache = /* @__PURE__ */ new Map();
|
|
551
|
+
const textWidth = (text) => {
|
|
552
|
+
const cached = widthCache.get(text);
|
|
553
|
+
if (cached !== void 0) return cached;
|
|
554
|
+
const width = stringWidth(text);
|
|
555
|
+
if (widthCache.size >= WIDTH_CACHE_LIMIT) widthCache.clear();
|
|
556
|
+
widthCache.set(text, width);
|
|
557
|
+
return width;
|
|
257
558
|
};
|
|
559
|
+
const resolveColumnSpec = (spec, resistance) => ({
|
|
560
|
+
id: spec.id,
|
|
561
|
+
grow: spec.grow,
|
|
562
|
+
canHide: spec.canHide,
|
|
563
|
+
variants: spec.variants.map((variant) => ({
|
|
564
|
+
id: variant.id,
|
|
565
|
+
minWidth: variant.minWidth,
|
|
566
|
+
idealWidth: variant.idealWidth,
|
|
567
|
+
maxWidth: variant.maxWidth,
|
|
568
|
+
shrinkResistance: resistance.shrink(spec.id, variant.id),
|
|
569
|
+
demoteResistance: resistance.demote(spec.id, variant.id),
|
|
570
|
+
hideResistance: resistance.hide(spec.id, variant.id),
|
|
571
|
+
renderCell: variant.renderCell
|
|
572
|
+
}))
|
|
573
|
+
});
|
|
574
|
+
const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.variants.length > 0 ? [resolveColumnSpec(spec, resistance)] : []);
|
|
258
575
|
|
|
259
576
|
//#endregion
|
|
260
577
|
//#region src/ink-renderer/columns/amount-column.tsx
|
|
@@ -285,8 +602,7 @@ const ProcessedCountColumn = ({ task, width }) => {
|
|
|
285
602
|
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
286
603
|
return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
|
|
287
604
|
};
|
|
288
|
-
const AmountSeparatorColumn = ({ task,
|
|
289
|
-
if (!showStructured) return null;
|
|
605
|
+
const AmountSeparatorColumn = ({ task, tick }) => {
|
|
290
606
|
if (task.units._tag === "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: "/" });
|
|
291
607
|
const symbol = formatAmount(task, tick);
|
|
292
608
|
if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
|
|
@@ -304,9 +620,8 @@ const TotalCountColumn = ({ task, width }) => {
|
|
|
304
620
|
if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
|
|
305
621
|
return /* @__PURE__ */ jsx(Text, { children: padRight(`${task.units.total}`, width) });
|
|
306
622
|
};
|
|
307
|
-
const AmountColumn = ({ task, tick,
|
|
308
|
-
|
|
309
|
-
if (!showStructured) {
|
|
623
|
+
const AmountColumn = ({ task, tick, layout }) => {
|
|
624
|
+
if (layout.kind === "text") {
|
|
310
625
|
const text = formatAmount(task, tick);
|
|
311
626
|
if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
|
|
312
627
|
wrap: "truncate-end",
|
|
@@ -323,36 +638,140 @@ const AmountColumn = ({ task, tick, amountSucceededWidth, amountFailedWidth, amo
|
|
|
323
638
|
children: text
|
|
324
639
|
});
|
|
325
640
|
}
|
|
641
|
+
if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
|
|
642
|
+
flexDirection: "row",
|
|
643
|
+
children: [
|
|
644
|
+
/* @__PURE__ */ jsx(ProcessedCountColumn, {
|
|
645
|
+
task,
|
|
646
|
+
width: layout.processedWidth
|
|
647
|
+
}),
|
|
648
|
+
/* @__PURE__ */ jsx(AmountSeparatorColumn, {
|
|
649
|
+
task,
|
|
650
|
+
tick
|
|
651
|
+
}),
|
|
652
|
+
/* @__PURE__ */ jsx(TotalCountColumn, {
|
|
653
|
+
task,
|
|
654
|
+
width: layout.totalWidth
|
|
655
|
+
})
|
|
656
|
+
]
|
|
657
|
+
});
|
|
326
658
|
return /* @__PURE__ */ jsxs(Box, {
|
|
327
659
|
flexDirection: "row",
|
|
328
660
|
children: [
|
|
329
|
-
|
|
661
|
+
layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SucceededCountColumn, {
|
|
330
662
|
task,
|
|
331
|
-
width:
|
|
663
|
+
width: layout.succeededWidth
|
|
332
664
|
}), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
|
|
333
|
-
|
|
665
|
+
layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FailedCountColumn, {
|
|
334
666
|
task,
|
|
335
|
-
width:
|
|
667
|
+
width: layout.failedWidth
|
|
336
668
|
}), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
|
|
337
669
|
/* @__PURE__ */ jsx(ProcessedCountColumn, {
|
|
338
670
|
task,
|
|
339
|
-
width:
|
|
671
|
+
width: layout.processedWidth
|
|
340
672
|
}),
|
|
341
673
|
/* @__PURE__ */ jsx(AmountSeparatorColumn, {
|
|
342
674
|
task,
|
|
343
|
-
showStructured,
|
|
344
675
|
tick
|
|
345
676
|
}),
|
|
346
677
|
/* @__PURE__ */ jsx(TotalCountColumn, {
|
|
347
678
|
task,
|
|
348
|
-
width:
|
|
679
|
+
width: layout.totalWidth
|
|
349
680
|
})
|
|
350
681
|
]
|
|
351
682
|
});
|
|
352
683
|
};
|
|
684
|
+
const computeAmountMetrics = (rows, tick) => {
|
|
685
|
+
let hasDeterminate = false;
|
|
686
|
+
let hasDetailed = false;
|
|
687
|
+
let totalDigits = 0;
|
|
688
|
+
let simpleTextWidth = 0;
|
|
689
|
+
for (const row of rows) {
|
|
690
|
+
const { task } = row;
|
|
691
|
+
if (isDeterminate(task)) {
|
|
692
|
+
hasDeterminate = true;
|
|
693
|
+
totalDigits = Math.max(totalDigits, textWidth(`${task.units.total}`));
|
|
694
|
+
if (task.countDisplay === "detailed") hasDetailed = true;
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
|
|
698
|
+
}
|
|
699
|
+
return {
|
|
700
|
+
hasDeterminate,
|
|
701
|
+
hasDetailed,
|
|
702
|
+
totalDigits: Math.max(1, totalDigits),
|
|
703
|
+
simpleTextWidth
|
|
704
|
+
};
|
|
705
|
+
};
|
|
706
|
+
const detailedAmountLayout = (metrics) => ({
|
|
707
|
+
kind: "detailed",
|
|
708
|
+
succeededWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
|
|
709
|
+
failedWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
|
|
710
|
+
processedWidth: metrics.totalDigits,
|
|
711
|
+
totalWidth: metrics.totalDigits
|
|
712
|
+
});
|
|
713
|
+
const processedAmountLayout = (metrics) => ({
|
|
714
|
+
kind: "processed",
|
|
715
|
+
processedWidth: metrics.totalDigits,
|
|
716
|
+
totalWidth: metrics.totalDigits
|
|
717
|
+
});
|
|
718
|
+
const detailedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits + (metrics.hasDetailed ? metrics.totalDigits + 1 + metrics.totalDigits + 1 : 0);
|
|
719
|
+
const processedAmountWidth = (metrics) => metrics.totalDigits + 1 + metrics.totalDigits;
|
|
720
|
+
const createAmountColumnSpec = (context) => {
|
|
721
|
+
const metrics = computeAmountMetrics(context.rows, context.tick);
|
|
722
|
+
if (!metrics.hasDeterminate && metrics.simpleTextWidth <= 0) return;
|
|
723
|
+
const detailedLayout = detailedAmountLayout(metrics);
|
|
724
|
+
const processedLayout = processedAmountLayout(metrics);
|
|
725
|
+
const detailedWidth = detailedAmountWidth(metrics);
|
|
726
|
+
const processedWidth = processedAmountWidth(metrics);
|
|
727
|
+
return {
|
|
728
|
+
id: "amount",
|
|
729
|
+
grow: 0,
|
|
730
|
+
canHide: true,
|
|
731
|
+
variants: metrics.hasDeterminate && metrics.hasDetailed ? [{
|
|
732
|
+
id: "detailed",
|
|
733
|
+
minWidth: detailedWidth,
|
|
734
|
+
idealWidth: detailedWidth,
|
|
735
|
+
renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
|
|
736
|
+
task: row.task,
|
|
737
|
+
tick: context.tick,
|
|
738
|
+
layout: detailedLayout
|
|
739
|
+
})
|
|
740
|
+
}, {
|
|
741
|
+
id: "processed",
|
|
742
|
+
minWidth: processedWidth,
|
|
743
|
+
idealWidth: processedWidth,
|
|
744
|
+
renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
|
|
745
|
+
task: row.task,
|
|
746
|
+
tick: context.tick,
|
|
747
|
+
layout: processedLayout
|
|
748
|
+
})
|
|
749
|
+
}] : metrics.hasDeterminate ? [{
|
|
750
|
+
id: "processed",
|
|
751
|
+
minWidth: processedWidth,
|
|
752
|
+
idealWidth: processedWidth,
|
|
753
|
+
renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
|
|
754
|
+
task: row.task,
|
|
755
|
+
tick: context.tick,
|
|
756
|
+
layout: processedLayout
|
|
757
|
+
})
|
|
758
|
+
}] : [{
|
|
759
|
+
id: "text",
|
|
760
|
+
minWidth: 0,
|
|
761
|
+
idealWidth: metrics.simpleTextWidth,
|
|
762
|
+
renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
|
|
763
|
+
task: row.task,
|
|
764
|
+
tick: context.tick,
|
|
765
|
+
layout: { kind: "text" }
|
|
766
|
+
})
|
|
767
|
+
}]
|
|
768
|
+
};
|
|
769
|
+
};
|
|
353
770
|
|
|
354
771
|
//#endregion
|
|
355
772
|
//#region src/ink-renderer/columns/bar-column.tsx
|
|
773
|
+
const DEFAULT_BAR_WIDTH = 30;
|
|
774
|
+
const MIN_BAR_WIDTH = 8;
|
|
356
775
|
const segmentLengths = (width, total, succeeded, failed) => {
|
|
357
776
|
if (total <= 0) return {
|
|
358
777
|
succeeded: 0,
|
|
@@ -390,6 +809,49 @@ const BarColumn = ({ task, width }) => {
|
|
|
390
809
|
]
|
|
391
810
|
});
|
|
392
811
|
};
|
|
812
|
+
const createBarColumnSpec = (context, isTTY) => {
|
|
813
|
+
if (!hasDeterminateRows(context.rows)) return;
|
|
814
|
+
return {
|
|
815
|
+
id: "bar",
|
|
816
|
+
grow: 0,
|
|
817
|
+
canHide: true,
|
|
818
|
+
variants: [{
|
|
819
|
+
id: "full",
|
|
820
|
+
minWidth: MIN_BAR_WIDTH,
|
|
821
|
+
idealWidth: DEFAULT_BAR_WIDTH,
|
|
822
|
+
maxWidth: DEFAULT_BAR_WIDTH,
|
|
823
|
+
renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
|
|
824
|
+
task: row.task,
|
|
825
|
+
tree: row.tree,
|
|
826
|
+
now: context.now,
|
|
827
|
+
tick: context.tick,
|
|
828
|
+
isTTY,
|
|
829
|
+
width: Math.max(1, Math.min(width, DEFAULT_BAR_WIDTH))
|
|
830
|
+
})
|
|
831
|
+
}, {
|
|
832
|
+
id: "compact",
|
|
833
|
+
minWidth: 1,
|
|
834
|
+
idealWidth: MIN_BAR_WIDTH,
|
|
835
|
+
maxWidth: MIN_BAR_WIDTH,
|
|
836
|
+
renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
|
|
837
|
+
task: row.task,
|
|
838
|
+
tree: row.tree,
|
|
839
|
+
now: context.now,
|
|
840
|
+
tick: context.tick,
|
|
841
|
+
isTTY,
|
|
842
|
+
width: Math.max(1, width)
|
|
843
|
+
})
|
|
844
|
+
}]
|
|
845
|
+
};
|
|
846
|
+
};
|
|
847
|
+
|
|
848
|
+
//#endregion
|
|
849
|
+
//#region src/ink-renderer/view/tree-prefix.ts
|
|
850
|
+
const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
|
|
851
|
+
const renderTreePrefix = (tree) => {
|
|
852
|
+
if (tree.depth <= 0) return "";
|
|
853
|
+
return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
|
|
854
|
+
};
|
|
393
855
|
|
|
394
856
|
//#endregion
|
|
395
857
|
//#region src/ink-renderer/columns/description-column.tsx
|
|
@@ -408,6 +870,51 @@ const DescriptionColumn = ({ task, tree, showTree, tick }) => {
|
|
|
408
870
|
]
|
|
409
871
|
});
|
|
410
872
|
};
|
|
873
|
+
const MIN_DESCRIPTION_WIDTH = 8;
|
|
874
|
+
const MIN_DESCRIPTION_WITH_TREE_WIDTH = 20;
|
|
875
|
+
const DESCRIPTION_PADDING_GROWTH_LIMIT = 20;
|
|
876
|
+
const maxDescriptionWidth = (rows, showTree) => rows.reduce((max, row) => {
|
|
877
|
+
const treePrefix = showTree ? renderTreePrefix(row.tree) : "";
|
|
878
|
+
return Math.max(max, textWidth(`${treePrefix}${row.task.description}`) + 2);
|
|
879
|
+
}, MIN_DESCRIPTION_WIDTH);
|
|
880
|
+
const createDescriptionColumnSpec = (context, isTTY) => {
|
|
881
|
+
const treeIdeal = maxDescriptionWidth(context.rows, true);
|
|
882
|
+
const plainIdeal = maxDescriptionWidth(context.rows, false);
|
|
883
|
+
const treeVariantIdeal = Math.max(MIN_DESCRIPTION_WITH_TREE_WIDTH, treeIdeal);
|
|
884
|
+
const plainVariantIdeal = Math.max(MIN_DESCRIPTION_WIDTH, plainIdeal);
|
|
885
|
+
return {
|
|
886
|
+
id: "description",
|
|
887
|
+
grow: 1,
|
|
888
|
+
canHide: false,
|
|
889
|
+
variants: [{
|
|
890
|
+
id: "tree",
|
|
891
|
+
minWidth: MIN_DESCRIPTION_WITH_TREE_WIDTH,
|
|
892
|
+
idealWidth: treeVariantIdeal,
|
|
893
|
+
maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, treeVariantIdeal),
|
|
894
|
+
renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
|
|
895
|
+
task: row.task,
|
|
896
|
+
tree: row.tree,
|
|
897
|
+
now: context.now,
|
|
898
|
+
tick: context.tick,
|
|
899
|
+
isTTY,
|
|
900
|
+
showTree: true
|
|
901
|
+
})
|
|
902
|
+
}, {
|
|
903
|
+
id: "plain",
|
|
904
|
+
minWidth: MIN_DESCRIPTION_WIDTH,
|
|
905
|
+
idealWidth: plainVariantIdeal,
|
|
906
|
+
maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, plainVariantIdeal),
|
|
907
|
+
renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
|
|
908
|
+
task: row.task,
|
|
909
|
+
tree: row.tree,
|
|
910
|
+
now: context.now,
|
|
911
|
+
tick: context.tick,
|
|
912
|
+
isTTY,
|
|
913
|
+
showTree: false
|
|
914
|
+
})
|
|
915
|
+
}]
|
|
916
|
+
};
|
|
917
|
+
};
|
|
411
918
|
|
|
412
919
|
//#endregion
|
|
413
920
|
//#region src/ink-renderer/columns/elapsed-column.tsx
|
|
@@ -416,156 +923,520 @@ const ElapsedColumn = ({ task, now }) => /* @__PURE__ */ jsx(Text, {
|
|
|
416
923
|
color: "gray",
|
|
417
924
|
children: formatElapsed(task, now)
|
|
418
925
|
});
|
|
926
|
+
const MIN_ELAPSED_WIDTH = 2;
|
|
927
|
+
const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
|
|
928
|
+
const maxElapsedWidth = (rows, now) => rows.reduce((max, row) => Math.max(max, textWidth(formatElapsed(row.task, now))), MIN_ELAPSED_WIDTH);
|
|
929
|
+
const createElapsedColumnSpec = (context, isTTY) => {
|
|
930
|
+
const elapsedContentWidth = maxElapsedWidth(context.rows, context.now);
|
|
931
|
+
return {
|
|
932
|
+
id: "elapsed",
|
|
933
|
+
grow: 0,
|
|
934
|
+
canHide: false,
|
|
935
|
+
variants: [{
|
|
936
|
+
id: "stable",
|
|
937
|
+
minWidth: elapsedContentWidth,
|
|
938
|
+
idealWidth: hasDeterminateRows(context.rows) ? Math.max(elapsedContentWidth, RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR) : elapsedContentWidth,
|
|
939
|
+
renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
|
|
940
|
+
task: row.task,
|
|
941
|
+
tree: row.tree,
|
|
942
|
+
now: context.now,
|
|
943
|
+
tick: context.tick,
|
|
944
|
+
isTTY
|
|
945
|
+
})
|
|
946
|
+
}, {
|
|
947
|
+
id: "compact",
|
|
948
|
+
minWidth: MIN_ELAPSED_WIDTH,
|
|
949
|
+
idealWidth: elapsedContentWidth,
|
|
950
|
+
renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
|
|
951
|
+
task: row.task,
|
|
952
|
+
tree: row.tree,
|
|
953
|
+
now: context.now,
|
|
954
|
+
tick: context.tick,
|
|
955
|
+
isTTY
|
|
956
|
+
})
|
|
957
|
+
}]
|
|
958
|
+
};
|
|
959
|
+
};
|
|
419
960
|
|
|
420
961
|
//#endregion
|
|
421
962
|
//#region src/ink-renderer/columns/eta-column.tsx
|
|
422
|
-
const
|
|
423
|
-
|
|
963
|
+
const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
|
|
964
|
+
const etaDurationText = (task, now) => {
|
|
965
|
+
if (task.status !== "running" || !isDeterminate(task)) return;
|
|
424
966
|
const eta = formatEta(task, now);
|
|
967
|
+
return eta.length > 0 ? eta : "--";
|
|
968
|
+
};
|
|
969
|
+
const EtaColumn = ({ task, now, mode }) => {
|
|
970
|
+
const duration = etaDurationText(task, now);
|
|
971
|
+
if (duration === void 0) return /* @__PURE__ */ jsx(Text, {});
|
|
425
972
|
return /* @__PURE__ */ jsx(Text, {
|
|
426
973
|
wrap: "truncate-end",
|
|
427
974
|
color: "gray",
|
|
428
|
-
children:
|
|
975
|
+
children: mode === "prefixed" ? `ETA: ${duration}` : mode === "primary" ? primaryUnit(duration) : duration
|
|
429
976
|
});
|
|
430
977
|
};
|
|
978
|
+
const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
|
|
979
|
+
const computeEtaMetrics = (rows, now) => {
|
|
980
|
+
let hasEta = false;
|
|
981
|
+
let prefixedWidth = 0;
|
|
982
|
+
let durationWidth = 0;
|
|
983
|
+
let primaryUnitWidth = 0;
|
|
984
|
+
for (const row of rows) {
|
|
985
|
+
const duration = etaDurationText(row.task, now);
|
|
986
|
+
if (duration === void 0) continue;
|
|
987
|
+
hasEta = true;
|
|
988
|
+
const prefixed = `ETA: ${duration}`;
|
|
989
|
+
prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
|
|
990
|
+
durationWidth = Math.max(durationWidth, textWidth(duration));
|
|
991
|
+
primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
|
|
992
|
+
}
|
|
993
|
+
return {
|
|
994
|
+
hasEta,
|
|
995
|
+
prefixedWidth,
|
|
996
|
+
durationWidth: Math.max(2, durationWidth),
|
|
997
|
+
primaryUnitWidth: Math.max(2, primaryUnitWidth)
|
|
998
|
+
};
|
|
999
|
+
};
|
|
1000
|
+
const createEtaColumnSpec = (context, isTTY) => {
|
|
1001
|
+
const metrics = computeEtaMetrics(context.rows, context.now);
|
|
1002
|
+
if (!metrics.hasEta) return;
|
|
1003
|
+
return {
|
|
1004
|
+
id: "eta",
|
|
1005
|
+
grow: 0,
|
|
1006
|
+
canHide: true,
|
|
1007
|
+
variants: [
|
|
1008
|
+
{
|
|
1009
|
+
id: "prefixed",
|
|
1010
|
+
minWidth: metrics.prefixedWidth,
|
|
1011
|
+
idealWidth: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
|
|
1012
|
+
renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
|
|
1013
|
+
task: row.task,
|
|
1014
|
+
tree: row.tree,
|
|
1015
|
+
now: context.now,
|
|
1016
|
+
tick: context.tick,
|
|
1017
|
+
isTTY,
|
|
1018
|
+
mode: "prefixed"
|
|
1019
|
+
})
|
|
1020
|
+
},
|
|
1021
|
+
{
|
|
1022
|
+
id: "duration",
|
|
1023
|
+
minWidth: metrics.durationWidth,
|
|
1024
|
+
idealWidth: metrics.durationWidth,
|
|
1025
|
+
renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
|
|
1026
|
+
task: row.task,
|
|
1027
|
+
tree: row.tree,
|
|
1028
|
+
now: context.now,
|
|
1029
|
+
tick: context.tick,
|
|
1030
|
+
isTTY,
|
|
1031
|
+
mode: "duration"
|
|
1032
|
+
})
|
|
1033
|
+
},
|
|
1034
|
+
{
|
|
1035
|
+
id: "primary",
|
|
1036
|
+
minWidth: metrics.primaryUnitWidth,
|
|
1037
|
+
idealWidth: metrics.primaryUnitWidth,
|
|
1038
|
+
renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
|
|
1039
|
+
task: row.task,
|
|
1040
|
+
tree: row.tree,
|
|
1041
|
+
now: context.now,
|
|
1042
|
+
tick: context.tick,
|
|
1043
|
+
isTTY,
|
|
1044
|
+
mode: "primary"
|
|
1045
|
+
})
|
|
1046
|
+
}
|
|
1047
|
+
]
|
|
1048
|
+
};
|
|
1049
|
+
};
|
|
431
1050
|
|
|
432
1051
|
//#endregion
|
|
433
|
-
//#region src/ink-renderer/
|
|
434
|
-
const
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
1052
|
+
//#region src/ink-renderer/columns/planner.ts
|
|
1053
|
+
const clampInt = (value, min, max) => Math.max(min, Math.min(max, Math.floor(value)));
|
|
1054
|
+
const activeColumns = (columns) => columns.filter((column) => !column.hidden);
|
|
1055
|
+
const visibleColumns = (columns) => columns.filter((column) => !column.hidden && column.width > 0);
|
|
1056
|
+
const visibleGapWidth = (columns) => Math.max(0, visibleColumns(columns).length - 1);
|
|
1057
|
+
const totalWidth = (columns) => visibleColumns(columns).reduce((sum, column) => sum + column.width, 0) + visibleGapWidth(columns);
|
|
1058
|
+
const currentVariant = (column) => column.variants[column.variantIndex];
|
|
1059
|
+
const variantMaxWidth = (column) => {
|
|
1060
|
+
const variant = currentVariant(column);
|
|
1061
|
+
return variant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(variant.minWidth, variant.maxWidth);
|
|
1062
|
+
};
|
|
1063
|
+
const reduceOverflowByShrink = (columns, overflow) => {
|
|
1064
|
+
let remaining = overflow;
|
|
1065
|
+
const candidates = activeColumns(columns).filter((column) => column.width > currentVariant(column).minWidth).sort((a, b) => currentVariant(a).shrinkResistance - currentVariant(b).shrinkResistance);
|
|
1066
|
+
for (const column of candidates) {
|
|
1067
|
+
if (remaining <= 0) break;
|
|
1068
|
+
const variant = currentVariant(column);
|
|
1069
|
+
const reducible = column.width - variant.minWidth;
|
|
1070
|
+
if (reducible <= 0) continue;
|
|
1071
|
+
const delta = Math.min(reducible, remaining);
|
|
1072
|
+
column.width -= delta;
|
|
1073
|
+
remaining -= delta;
|
|
1074
|
+
}
|
|
1075
|
+
return remaining;
|
|
1076
|
+
};
|
|
1077
|
+
const nextDemoteCandidate = (columns) => activeColumns(columns).filter((column) => column.variantIndex + 1 < column.variants.length).sort((a, b) => currentVariant(a).demoteResistance - currentVariant(b).demoteResistance)[0];
|
|
1078
|
+
const applyDemote = (column) => {
|
|
1079
|
+
const nextVariant = column.variants[column.variantIndex + 1];
|
|
1080
|
+
column.variantIndex += 1;
|
|
1081
|
+
const nextMaxWidth = nextVariant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(nextVariant.minWidth, nextVariant.maxWidth);
|
|
1082
|
+
column.width = Math.max(nextVariant.minWidth, Math.min(column.width, nextVariant.idealWidth, nextMaxWidth));
|
|
1083
|
+
};
|
|
1084
|
+
const nextHideCandidate = (columns) => activeColumns(columns).filter((column) => column.spec.canHide).sort((a, b) => currentVariant(a).hideResistance - currentVariant(b).hideResistance)[0];
|
|
1085
|
+
const applyHide = (column) => {
|
|
1086
|
+
column.hidden = true;
|
|
1087
|
+
column.width = 0;
|
|
1088
|
+
};
|
|
1089
|
+
const distributeGrowth = (columns, extra) => {
|
|
1090
|
+
if (extra <= 0) return;
|
|
1091
|
+
let remaining = extra;
|
|
1092
|
+
while (remaining > 0) {
|
|
1093
|
+
const candidates = activeColumns(columns).filter((column) => column.spec.grow > 0 && column.width < variantMaxWidth(column));
|
|
1094
|
+
if (candidates.length === 0) break;
|
|
1095
|
+
const growSum = candidates.reduce((sum, column) => sum + Math.max(0, column.spec.grow), 0);
|
|
1096
|
+
if (growSum <= 0) break;
|
|
1097
|
+
let distributed = 0;
|
|
1098
|
+
for (const column of candidates) {
|
|
1099
|
+
const weight = Math.max(0, column.spec.grow);
|
|
1100
|
+
if (weight <= 0) continue;
|
|
1101
|
+
const headroom = Math.max(0, variantMaxWidth(column) - column.width);
|
|
1102
|
+
if (headroom <= 0) continue;
|
|
1103
|
+
const share = Math.min(headroom, Math.floor(remaining * weight / growSum));
|
|
1104
|
+
if (share <= 0) continue;
|
|
1105
|
+
column.width += share;
|
|
1106
|
+
distributed += share;
|
|
1107
|
+
}
|
|
1108
|
+
if (distributed === 0) {
|
|
1109
|
+
for (const column of candidates) {
|
|
1110
|
+
if (remaining <= 0) break;
|
|
1111
|
+
if (Math.max(0, variantMaxWidth(column) - column.width) <= 0) continue;
|
|
1112
|
+
column.width += 1;
|
|
1113
|
+
distributed += 1;
|
|
1114
|
+
remaining -= 1;
|
|
1115
|
+
}
|
|
1116
|
+
if (distributed === 0) break;
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
remaining -= distributed;
|
|
1120
|
+
}
|
|
1121
|
+
};
|
|
1122
|
+
const planColumns = ({ context, baselineWidth, columns }) => {
|
|
1123
|
+
const mutable = columns.flatMap((column) => {
|
|
1124
|
+
if (column.variants.length === 0) return [];
|
|
1125
|
+
const first = column.variants[0];
|
|
1126
|
+
return [{
|
|
1127
|
+
spec: column,
|
|
1128
|
+
variants: column.variants,
|
|
1129
|
+
variantIndex: 0,
|
|
1130
|
+
width: Math.max(first.minWidth, Math.min(first.idealWidth, first.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(first.minWidth, first.maxWidth))),
|
|
1131
|
+
hidden: false
|
|
1132
|
+
}];
|
|
1133
|
+
});
|
|
1134
|
+
if (mutable.length === 0) return {
|
|
1135
|
+
rowWidth: 0,
|
|
1136
|
+
columns: []
|
|
1137
|
+
};
|
|
1138
|
+
const naturalWidth = totalWidth(mutable);
|
|
1139
|
+
const clampedTerminal = context.terminalColumns === void 0 ? void 0 : clampInt(context.terminalColumns, 1, Number.POSITIVE_INFINITY);
|
|
1140
|
+
const baselineTarget = Math.max(1, Math.max(baselineWidth, naturalWidth));
|
|
1141
|
+
const target = clampedTerminal === void 0 ? baselineTarget : Math.min(baselineTarget, clampedTerminal);
|
|
1142
|
+
if (naturalWidth < target) distributeGrowth(mutable, target - naturalWidth);
|
|
1143
|
+
else if (naturalWidth > target) {
|
|
1144
|
+
let overflow = naturalWidth - target;
|
|
1145
|
+
let progressed = true;
|
|
1146
|
+
while (overflow > 0 && progressed) {
|
|
1147
|
+
const before = overflow;
|
|
1148
|
+
overflow = reduceOverflowByShrink(mutable, overflow);
|
|
1149
|
+
if (overflow <= 0) break;
|
|
1150
|
+
const demoteCandidate = nextDemoteCandidate(mutable);
|
|
1151
|
+
const hideCandidate = nextHideCandidate(mutable);
|
|
1152
|
+
if (demoteCandidate === void 0 && hideCandidate === void 0) {
|
|
1153
|
+
progressed = before !== overflow;
|
|
1154
|
+
continue;
|
|
1155
|
+
}
|
|
1156
|
+
if ((demoteCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(demoteCandidate).demoteResistance) <= (hideCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(hideCandidate).hideResistance) && demoteCandidate !== void 0) applyDemote(demoteCandidate);
|
|
1157
|
+
else if (hideCandidate !== void 0) applyHide(hideCandidate);
|
|
1158
|
+
overflow = Math.max(0, totalWidth(mutable) - target);
|
|
1159
|
+
progressed = true;
|
|
1160
|
+
}
|
|
1161
|
+
const compactedWidth = totalWidth(mutable);
|
|
1162
|
+
if (compactedWidth < target) distributeGrowth(mutable, target - compactedWidth);
|
|
1163
|
+
}
|
|
1164
|
+
const visible = visibleColumns(mutable);
|
|
1165
|
+
return {
|
|
1166
|
+
rowWidth: totalWidth(mutable),
|
|
1167
|
+
columns: visible.map((column) => {
|
|
1168
|
+
const variant = currentVariant(column);
|
|
1169
|
+
return {
|
|
1170
|
+
id: column.spec.id,
|
|
1171
|
+
variantId: variant.id,
|
|
1172
|
+
width: column.width,
|
|
1173
|
+
renderCell: variant.renderCell
|
|
1174
|
+
};
|
|
1175
|
+
})
|
|
1176
|
+
};
|
|
1177
|
+
};
|
|
1178
|
+
|
|
1179
|
+
//#endregion
|
|
1180
|
+
//#region src/ink-renderer/columns/layout.tsx
|
|
1181
|
+
/**
|
|
1182
|
+
* Frame planning algorithm (per render tick):
|
|
1183
|
+
*
|
|
1184
|
+
* 1. Ask each column module to produce a logical column spec (variants + measured widths).
|
|
1185
|
+
* 2. Apply shared resistance rules (shrink / demote / hide) to those variants.
|
|
1186
|
+
* 3. Feed resolved columns into `planColumns`.
|
|
1187
|
+
* 4. `planColumns` shrinks, demotes, and hides until the row fits target width.
|
|
1188
|
+
* 5. Render rows with the selected variant and width for each visible column.
|
|
1189
|
+
*/
|
|
1190
|
+
const BASELINE_ROW_WIDTH = 150;
|
|
1191
|
+
const DEFAULT_RESISTANCE = 1e3;
|
|
1192
|
+
const variantOrderKey = ({ columnId, variantId }) => `${columnId}:${variantId}`;
|
|
1193
|
+
const SHRINK_ORDER = [
|
|
1194
|
+
{
|
|
1195
|
+
columnId: "eta",
|
|
1196
|
+
variantId: "prefixed"
|
|
1197
|
+
},
|
|
1198
|
+
{
|
|
1199
|
+
columnId: "elapsed",
|
|
1200
|
+
variantId: "stable"
|
|
1201
|
+
},
|
|
1202
|
+
{
|
|
1203
|
+
columnId: "eta",
|
|
1204
|
+
variantId: "duration"
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
columnId: "eta",
|
|
1208
|
+
variantId: "primary"
|
|
1209
|
+
},
|
|
1210
|
+
{
|
|
1211
|
+
columnId: "bar",
|
|
1212
|
+
variantId: "compact"
|
|
1213
|
+
},
|
|
1214
|
+
{
|
|
1215
|
+
columnId: "bar",
|
|
1216
|
+
variantId: "full"
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
columnId: "amount",
|
|
1220
|
+
variantId: "text"
|
|
1221
|
+
},
|
|
1222
|
+
{
|
|
1223
|
+
columnId: "elapsed",
|
|
1224
|
+
variantId: "compact"
|
|
1225
|
+
},
|
|
1226
|
+
{
|
|
1227
|
+
columnId: "description",
|
|
1228
|
+
variantId: "tree"
|
|
1229
|
+
},
|
|
1230
|
+
{
|
|
1231
|
+
columnId: "amount",
|
|
1232
|
+
variantId: "detailed"
|
|
1233
|
+
},
|
|
1234
|
+
{
|
|
1235
|
+
columnId: "amount",
|
|
1236
|
+
variantId: "processed"
|
|
1237
|
+
},
|
|
1238
|
+
{
|
|
1239
|
+
columnId: "description",
|
|
1240
|
+
variantId: "plain"
|
|
1241
|
+
}
|
|
1242
|
+
];
|
|
1243
|
+
const DEMOTE_ORDER = [
|
|
1244
|
+
{
|
|
1245
|
+
columnId: "description",
|
|
1246
|
+
variantId: "tree"
|
|
1247
|
+
},
|
|
1248
|
+
{
|
|
1249
|
+
columnId: "eta",
|
|
1250
|
+
variantId: "prefixed"
|
|
1251
|
+
},
|
|
1252
|
+
{
|
|
1253
|
+
columnId: "eta",
|
|
1254
|
+
variantId: "duration"
|
|
1255
|
+
},
|
|
1256
|
+
{
|
|
1257
|
+
columnId: "elapsed",
|
|
1258
|
+
variantId: "stable"
|
|
1259
|
+
},
|
|
1260
|
+
{
|
|
1261
|
+
columnId: "bar",
|
|
1262
|
+
variantId: "full"
|
|
1263
|
+
},
|
|
1264
|
+
{
|
|
1265
|
+
columnId: "amount",
|
|
1266
|
+
variantId: "detailed"
|
|
1267
|
+
}
|
|
1268
|
+
];
|
|
1269
|
+
const HIDE_ORDER = [
|
|
1270
|
+
{
|
|
1271
|
+
columnId: "eta",
|
|
1272
|
+
variantId: "primary"
|
|
1273
|
+
},
|
|
1274
|
+
{
|
|
1275
|
+
columnId: "eta",
|
|
1276
|
+
variantId: "prefixed"
|
|
1277
|
+
},
|
|
1278
|
+
{
|
|
1279
|
+
columnId: "eta",
|
|
1280
|
+
variantId: "duration"
|
|
1281
|
+
},
|
|
1282
|
+
{
|
|
1283
|
+
columnId: "bar",
|
|
1284
|
+
variantId: "full"
|
|
1285
|
+
},
|
|
1286
|
+
{
|
|
1287
|
+
columnId: "bar",
|
|
1288
|
+
variantId: "compact"
|
|
1289
|
+
},
|
|
1290
|
+
{
|
|
1291
|
+
columnId: "amount",
|
|
1292
|
+
variantId: "text"
|
|
1293
|
+
},
|
|
1294
|
+
{
|
|
1295
|
+
columnId: "amount",
|
|
1296
|
+
variantId: "detailed"
|
|
1297
|
+
},
|
|
1298
|
+
{
|
|
1299
|
+
columnId: "amount",
|
|
1300
|
+
variantId: "processed"
|
|
1301
|
+
}
|
|
1302
|
+
];
|
|
1303
|
+
const toResistanceLookup = (order) => new Map(order.map((item, index) => [variantOrderKey(item), index + 1]));
|
|
1304
|
+
const shrinkResistanceLookup = toResistanceLookup(SHRINK_ORDER);
|
|
1305
|
+
const demoteResistanceLookup = toResistanceLookup(DEMOTE_ORDER);
|
|
1306
|
+
const hideResistanceLookup = toResistanceLookup(HIDE_ORDER);
|
|
1307
|
+
const resistanceFor = (lookup, columnId, variantId) => lookup.get(`${columnId}:${variantId}`) ?? DEFAULT_RESISTANCE;
|
|
1308
|
+
const buildColumns = (context, isTTY) => {
|
|
1309
|
+
return resolveColumnSpecs([
|
|
1310
|
+
createDescriptionColumnSpec(context, isTTY),
|
|
1311
|
+
createBarColumnSpec(context, isTTY),
|
|
1312
|
+
createAmountColumnSpec(context),
|
|
1313
|
+
createElapsedColumnSpec(context, isTTY),
|
|
1314
|
+
createEtaColumnSpec(context, isTTY)
|
|
1315
|
+
].filter((spec) => spec !== void 0), {
|
|
1316
|
+
shrink: (columnId, variantId) => resistanceFor(shrinkResistanceLookup, columnId, variantId),
|
|
1317
|
+
demote: (columnId, variantId) => resistanceFor(demoteResistanceLookup, columnId, variantId),
|
|
1318
|
+
hide: (columnId, variantId) => resistanceFor(hideResistanceLookup, columnId, variantId)
|
|
1319
|
+
});
|
|
1320
|
+
};
|
|
1321
|
+
const computeFrameLayout = (rows, now, tick, terminalColumns, isTTY) => {
|
|
1322
|
+
const context = {
|
|
1323
|
+
rows,
|
|
438
1324
|
now,
|
|
439
1325
|
tick,
|
|
440
|
-
|
|
441
|
-
showTree: widths.showTree,
|
|
442
|
-
amountSucceededWidth: widths.amountSucceeded,
|
|
443
|
-
amountFailedWidth: widths.amountFailed,
|
|
444
|
-
amountProcessedWidth: widths.amountProcessed,
|
|
445
|
-
amountTotalWidth: widths.amountTotal
|
|
1326
|
+
terminalColumns
|
|
446
1327
|
};
|
|
447
|
-
return
|
|
1328
|
+
return planColumns({
|
|
1329
|
+
context,
|
|
1330
|
+
columns: buildColumns(context, isTTY),
|
|
1331
|
+
baselineWidth: hasDeterminateRows(rows) ? BASELINE_ROW_WIDTH : 1
|
|
1332
|
+
});
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
//#endregion
|
|
1336
|
+
//#region src/ink-renderer/view/task-row.tsx
|
|
1337
|
+
const TaskRow = ({ row, layout }) => {
|
|
1338
|
+
return /* @__PURE__ */ jsx(Box, {
|
|
448
1339
|
flexDirection: "row",
|
|
449
|
-
minWidth:
|
|
450
|
-
children:
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}),
|
|
457
|
-
widths.bar > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
458
|
-
width: widths.bar,
|
|
459
|
-
flexShrink: 0,
|
|
460
|
-
marginRight: 1,
|
|
461
|
-
children: /* @__PURE__ */ jsx(BarColumn, {
|
|
462
|
-
...props,
|
|
463
|
-
width: Math.max(1, Math.min(widths.bar, DEFAULT_BAR_WIDTH))
|
|
464
|
-
})
|
|
465
|
-
}) : null,
|
|
466
|
-
widths.amount > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
467
|
-
width: widths.amount,
|
|
468
|
-
flexShrink: 0,
|
|
469
|
-
marginRight: 1,
|
|
470
|
-
children: /* @__PURE__ */ jsx(AmountColumn, { ...props })
|
|
471
|
-
}) : null,
|
|
472
|
-
/* @__PURE__ */ jsx(Box, {
|
|
473
|
-
width: widths.elapsed,
|
|
474
|
-
flexShrink: 0,
|
|
475
|
-
marginRight: 1,
|
|
476
|
-
children: /* @__PURE__ */ jsx(ElapsedColumn, { ...props })
|
|
477
|
-
}),
|
|
478
|
-
widths.eta > 0 ? /* @__PURE__ */ jsx(Box, {
|
|
479
|
-
width: widths.eta,
|
|
480
|
-
flexShrink: 0,
|
|
481
|
-
children: /* @__PURE__ */ jsx(EtaColumn, { ...props })
|
|
482
|
-
}) : null
|
|
483
|
-
]
|
|
1340
|
+
minWidth: layout.rowWidth,
|
|
1341
|
+
children: layout.columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
|
|
1342
|
+
width: column.width,
|
|
1343
|
+
flexShrink: column.id === "description" ? 1 : 0,
|
|
1344
|
+
marginRight: index < layout.columns.length - 1 ? 1 : 0,
|
|
1345
|
+
children: column.renderCell(row, column.width)
|
|
1346
|
+
}, column.id))
|
|
484
1347
|
});
|
|
485
1348
|
};
|
|
486
1349
|
|
|
487
1350
|
//#endregion
|
|
488
|
-
//#region src/ink-renderer/
|
|
489
|
-
const
|
|
490
|
-
const
|
|
1351
|
+
//#region src/ink-renderer/view/progress-view.tsx
|
|
1352
|
+
const ProgressView = ({ rows, now, tick, isTTY, terminalColumns }) => {
|
|
1353
|
+
const layout = computeFrameLayout(rows, now, tick, terminalColumns, isTTY);
|
|
491
1354
|
return /* @__PURE__ */ jsx(Box, {
|
|
492
1355
|
flexDirection: "column",
|
|
493
1356
|
children: rows.map((row) => /* @__PURE__ */ jsx(TaskRow, {
|
|
494
1357
|
row,
|
|
495
|
-
|
|
496
|
-
tick,
|
|
497
|
-
isTTY,
|
|
498
|
-
widths
|
|
1358
|
+
layout
|
|
499
1359
|
}, row.task.id))
|
|
500
1360
|
});
|
|
501
1361
|
};
|
|
502
1362
|
|
|
503
1363
|
//#endregion
|
|
504
|
-
//#region src/ink-renderer/
|
|
505
|
-
const
|
|
506
|
-
const
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
});
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
})
|
|
1364
|
+
//#region src/ink-renderer/view/hooks/use-now-clock.ts
|
|
1365
|
+
const useNowClock = (active, intervalMillis) => {
|
|
1366
|
+
const [now, setNow] = useState(() => Date.now());
|
|
1367
|
+
useEffect(() => {
|
|
1368
|
+
if (!active) return;
|
|
1369
|
+
setNow(Date.now());
|
|
1370
|
+
const interval = setInterval(() => {
|
|
1371
|
+
setNow(Date.now());
|
|
1372
|
+
}, intervalMillis);
|
|
1373
|
+
return () => {
|
|
1374
|
+
clearInterval(interval);
|
|
1375
|
+
};
|
|
1376
|
+
}, [active, intervalMillis]);
|
|
1377
|
+
return now;
|
|
1378
|
+
};
|
|
517
1379
|
|
|
518
1380
|
//#endregion
|
|
519
|
-
//#region src/ink-renderer/
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
1381
|
+
//#region src/ink-renderer/view/hooks/use-spinner-clock.ts
|
|
1382
|
+
const useSpinnerClock = (active, intervalMillis) => {
|
|
1383
|
+
const [tick, setTick] = useState(0);
|
|
1384
|
+
useEffect(() => {
|
|
1385
|
+
if (!active) return;
|
|
1386
|
+
const interval = setInterval(() => {
|
|
1387
|
+
setTick((current) => current + 1);
|
|
1388
|
+
}, intervalMillis);
|
|
1389
|
+
return () => {
|
|
1390
|
+
clearInterval(interval);
|
|
1391
|
+
};
|
|
1392
|
+
}, [active, intervalMillis]);
|
|
1393
|
+
return tick;
|
|
1394
|
+
};
|
|
1395
|
+
|
|
1396
|
+
//#endregion
|
|
1397
|
+
//#region src/ink-renderer/view/render-root.tsx
|
|
1398
|
+
const SPINNER_INTERVAL_MILLIS = 100;
|
|
1399
|
+
const NOW_INTERVAL_MILLIS = 1e3;
|
|
1400
|
+
const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
|
|
1401
|
+
const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
1402
|
+
const tick = useSpinnerClock(snapshot.hasRunningTasks, SPINNER_INTERVAL_MILLIS);
|
|
1403
|
+
const now = useNowClock(snapshot.hasRunningTasks, NOW_INTERVAL_MILLIS);
|
|
1404
|
+
return /* @__PURE__ */ jsx(ProgressView, {
|
|
1405
|
+
rows: snapshot.rows,
|
|
1406
|
+
now,
|
|
1407
|
+
tick,
|
|
1408
|
+
isTTY,
|
|
1409
|
+
terminalColumns: getTerminalColumns()
|
|
545
1410
|
});
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
})
|
|
1411
|
+
};
|
|
1412
|
+
|
|
1413
|
+
//#endregion
|
|
1414
|
+
//#region src/services/ink-renderer.tsx
|
|
1415
|
+
const MAX_FPS = 12;
|
|
1416
|
+
const makeDefaultInkRenderer = () => ({ run: (store, stdio, isTTY) => Effect.sync(() => render(/* @__PURE__ */ jsx(ProgressRoot, {
|
|
1417
|
+
store,
|
|
1418
|
+
isTTY,
|
|
1419
|
+
getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0
|
|
1420
|
+
}), {
|
|
1421
|
+
stdout: stdio.stdout,
|
|
1422
|
+
stderr: stdio.stderr,
|
|
1423
|
+
patchConsole: true,
|
|
1424
|
+
exitOnCtrlC: false,
|
|
1425
|
+
debug: false,
|
|
1426
|
+
maxFps: MAX_FPS
|
|
1427
|
+
})).pipe(Effect.flatMap((instance) => Effect.never.pipe(Effect.ensuring(Effect.gen(function* () {
|
|
1428
|
+
store.flush();
|
|
1429
|
+
yield* Effect.sleep("0 millis");
|
|
1430
|
+
yield* Effect.sync(() => {
|
|
1431
|
+
instance.unmount();
|
|
1432
|
+
});
|
|
1433
|
+
}))))) });
|
|
563
1434
|
var InkRenderer = class InkRenderer extends Context.Tag("stromseng.dev/effective-progress/InkRenderer")() {
|
|
564
1435
|
static Default = Layer.succeed(InkRenderer, InkRenderer.of(makeDefaultInkRenderer()));
|
|
565
1436
|
};
|
|
566
1437
|
|
|
567
1438
|
//#endregion
|
|
568
|
-
//#region src/stdio.ts
|
|
1439
|
+
//#region src/services/stdio.ts
|
|
569
1440
|
const defaultStdioService = {
|
|
570
1441
|
stdout: process.stdout,
|
|
571
1442
|
stderr: process.stderr
|
|
@@ -575,348 +1446,32 @@ var ProgressStdio = class ProgressStdio extends Context.Tag("stromseng.dev/effec
|
|
|
575
1446
|
};
|
|
576
1447
|
|
|
577
1448
|
//#endregion
|
|
578
|
-
//#region src/
|
|
579
|
-
const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
|
|
580
|
-
const TaskId = Brand.nominal();
|
|
581
|
-
const TaskStatusSchema = Schema.Literal("running", "done", "failed");
|
|
582
|
-
const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
|
|
583
|
-
var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
|
|
584
|
-
succeeded: Schema.Number,
|
|
585
|
-
failed: Schema.Number,
|
|
586
|
-
processed: Schema.Number,
|
|
587
|
-
total: Schema.Number
|
|
588
|
-
}) {};
|
|
589
|
-
var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", { spinnerFrame: Schema.Number }) {};
|
|
590
|
-
const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
|
|
591
|
-
var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
|
|
592
|
-
id: TaskIdSchema,
|
|
593
|
-
parentId: Schema.NullOr(TaskIdSchema),
|
|
594
|
-
description: Schema.String,
|
|
595
|
-
status: TaskStatusSchema,
|
|
596
|
-
countDisplay: TaskCountDisplaySchema,
|
|
597
|
-
transient: Schema.Boolean,
|
|
598
|
-
units: TaskUnitsSchema,
|
|
599
|
-
startedAt: Schema.Number,
|
|
600
|
-
completedAt: Schema.NullOr(Schema.Number)
|
|
601
|
-
}) {};
|
|
602
|
-
var Task = class extends Context.Tag("stromseng.dev/effective-progress/Task")() {};
|
|
603
|
-
var TaskAddedEvent = class extends Schema.TaggedClass()("TaskAdded", {
|
|
604
|
-
taskId: TaskIdSchema,
|
|
605
|
-
parentId: Schema.NullOr(TaskIdSchema),
|
|
606
|
-
description: Schema.String,
|
|
607
|
-
total: Schema.optional(Schema.Number),
|
|
608
|
-
transient: Schema.Boolean,
|
|
609
|
-
countDisplay: TaskCountDisplaySchema
|
|
610
|
-
}) {};
|
|
611
|
-
var TaskUpdatedEvent = class extends Schema.TaggedClass()("TaskUpdated", {
|
|
612
|
-
taskId: TaskIdSchema,
|
|
613
|
-
description: Schema.optional(Schema.String),
|
|
614
|
-
succeeded: Schema.optional(Schema.Number),
|
|
615
|
-
failed: Schema.optional(Schema.Number),
|
|
616
|
-
processed: Schema.optional(Schema.Number),
|
|
617
|
-
total: Schema.optional(Schema.Number),
|
|
618
|
-
transient: Schema.optional(Schema.Boolean),
|
|
619
|
-
countDisplay: Schema.optional(TaskCountDisplaySchema)
|
|
620
|
-
}) {};
|
|
621
|
-
var TaskAdvancedEvent = class extends Schema.TaggedClass()("TaskAdvanced", {
|
|
622
|
-
taskId: TaskIdSchema,
|
|
623
|
-
amount: Schema.Number,
|
|
624
|
-
kind: Schema.Literal("succeeded", "failed")
|
|
625
|
-
}) {};
|
|
626
|
-
var TaskCompletedEvent = class extends Schema.TaggedClass()("TaskCompleted", { taskId: TaskIdSchema }) {};
|
|
627
|
-
var TaskFailedEvent = class extends Schema.TaggedClass()("TaskFailed", { taskId: TaskIdSchema }) {};
|
|
628
|
-
var TaskRemovedEvent = class extends Schema.TaggedClass()("TaskRemoved", { taskId: TaskIdSchema }) {};
|
|
629
|
-
const ProgressTaskEventSchema = Schema.Union(TaskAddedEvent, TaskUpdatedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskFailedEvent, TaskRemovedEvent);
|
|
630
|
-
const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema);
|
|
631
|
-
|
|
632
|
-
//#endregion
|
|
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
|
-
});
|
|
650
|
-
const updatedSnapshot = (snapshot, options) => {
|
|
651
|
-
const currentUnits = snapshot.units;
|
|
652
|
-
const units = (() => {
|
|
653
|
-
if (options.total !== void 0) {
|
|
654
|
-
if (options.total <= 0) return new IndeterminateTaskUnits({ spinnerFrame: 0 });
|
|
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
|
|
660
|
-
});
|
|
661
|
-
}
|
|
662
|
-
if (currentUnits._tag === "DeterminateTaskUnits") {
|
|
663
|
-
if (options.succeeded === void 0 && options.failed === void 0) return currentUnits;
|
|
664
|
-
return updateDeterminateCounts(currentUnits, options);
|
|
665
|
-
}
|
|
666
|
-
return currentUnits;
|
|
667
|
-
})();
|
|
668
|
-
return new TaskSnapshot({
|
|
669
|
-
id: snapshot.id,
|
|
670
|
-
parentId: snapshot.parentId,
|
|
671
|
-
description: options.description ?? snapshot.description,
|
|
672
|
-
status: snapshot.status,
|
|
673
|
-
countDisplay: options.countDisplay ?? snapshot.countDisplay,
|
|
674
|
-
transient: options.transient ?? snapshot.transient,
|
|
675
|
-
units,
|
|
676
|
-
startedAt: snapshot.startedAt,
|
|
677
|
-
completedAt: snapshot.completedAt
|
|
678
|
-
});
|
|
679
|
-
};
|
|
680
|
-
const withTransient = (snapshot, transient) => new TaskSnapshot({
|
|
681
|
-
id: snapshot.id,
|
|
682
|
-
parentId: snapshot.parentId,
|
|
683
|
-
description: snapshot.description,
|
|
684
|
-
status: snapshot.status,
|
|
685
|
-
countDisplay: snapshot.countDisplay,
|
|
686
|
-
transient,
|
|
687
|
-
units: snapshot.units,
|
|
688
|
-
startedAt: snapshot.startedAt,
|
|
689
|
-
completedAt: snapshot.completedAt
|
|
690
|
-
});
|
|
691
|
-
const findInsertionIndex = (renderOrder, parentId) => {
|
|
692
|
-
if (parentId === null) return {
|
|
693
|
-
index: renderOrder.length,
|
|
694
|
-
depth: 0
|
|
695
|
-
};
|
|
696
|
-
const parentIdx = renderOrder.findIndex((row) => row.id === parentId);
|
|
697
|
-
if (parentIdx === -1) return {
|
|
698
|
-
index: renderOrder.length,
|
|
699
|
-
depth: 0
|
|
700
|
-
};
|
|
701
|
-
const parentDepth = renderOrder[parentIdx].depth;
|
|
702
|
-
let i = parentIdx + 1;
|
|
703
|
-
while (i < renderOrder.length && renderOrder[i].depth > parentDepth) i++;
|
|
704
|
-
return {
|
|
705
|
-
index: i,
|
|
706
|
-
depth: parentDepth + 1
|
|
707
|
-
};
|
|
708
|
-
};
|
|
709
|
-
const removeFromRenderOrder = (renderOrder, taskId) => {
|
|
710
|
-
const idx = renderOrder.findIndex((row) => row.id === taskId);
|
|
711
|
-
if (idx === -1) return renderOrder;
|
|
712
|
-
const taskDepth = renderOrder[idx].depth;
|
|
713
|
-
let end = idx + 1;
|
|
714
|
-
while (end < renderOrder.length && renderOrder[end].depth > taskDepth) end++;
|
|
715
|
-
const next = [...renderOrder];
|
|
716
|
-
next.splice(idx, end - idx);
|
|
717
|
-
return next;
|
|
718
|
-
};
|
|
1449
|
+
//#region src/services/progress.ts
|
|
719
1450
|
const makeProgressService = Effect.gen(function* () {
|
|
720
1451
|
const stdio = yield* ProgressStdio;
|
|
721
1452
|
const inkRenderer = yield* InkRenderer;
|
|
722
1453
|
const outerConsole = yield* Effect.console;
|
|
723
1454
|
const isTTY = Boolean(stdio.stderr.isTTY);
|
|
724
|
-
const
|
|
725
|
-
const storeRef = yield* Ref.make({
|
|
726
|
-
tasks: /* @__PURE__ */ new Map(),
|
|
727
|
-
renderOrder: []
|
|
728
|
-
});
|
|
729
|
-
const dirtyRef = yield* Ref.make(true);
|
|
1455
|
+
const store = makeProgressRenderStore();
|
|
730
1456
|
const currentParentRef = yield* FiberRef.make(Option.none());
|
|
731
1457
|
const scope = yield* Effect.scope;
|
|
732
|
-
const markDirty = Ref.set(dirtyRef, true);
|
|
733
1458
|
const log = (...args) => args.length === 0 ? Effect.void : outerConsole.log(...args);
|
|
734
|
-
yield* Effect.forkIn(inkRenderer.run(
|
|
1459
|
+
yield* Effect.forkIn(inkRenderer.run(store, stdio, isTTY), scope);
|
|
735
1460
|
yield* Effect.sleep("0 millis");
|
|
736
1461
|
const addTask = (options) => Effect.gen(function* () {
|
|
737
1462
|
const resolvedParentId = options.parentId === void 0 ? yield* FiberRef.get(currentParentRef) : Option.some(options.parentId);
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
failed: 0,
|
|
742
|
-
total: options.total
|
|
743
|
-
});
|
|
744
|
-
const store = yield* Ref.get(storeRef);
|
|
745
|
-
const parentSnapshot = Option.isSome(resolvedParentId) ? store.tasks.get(resolvedParentId.value) : void 0;
|
|
746
|
-
const now = yield* Clock.currentTimeMillis;
|
|
747
|
-
const parentIdValue = Option.getOrNull(resolvedParentId);
|
|
748
|
-
const countDisplay = options.countDisplay ?? parentSnapshot?.countDisplay ?? "detailed";
|
|
749
|
-
const snapshot = new TaskSnapshot({
|
|
750
|
-
id: taskId,
|
|
751
|
-
parentId: parentIdValue,
|
|
752
|
-
description: options.description,
|
|
753
|
-
status: "running",
|
|
754
|
-
countDisplay,
|
|
755
|
-
transient: (parentSnapshot?.transient ?? false) || (options.transient ?? false),
|
|
756
|
-
units,
|
|
757
|
-
startedAt: now,
|
|
758
|
-
completedAt: null
|
|
759
|
-
});
|
|
760
|
-
yield* Ref.update(storeRef, (s) => {
|
|
761
|
-
const nextTasks = new Map(s.tasks);
|
|
762
|
-
nextTasks.set(taskId, snapshot);
|
|
763
|
-
const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
|
|
764
|
-
const nextOrder = [...s.renderOrder];
|
|
765
|
-
nextOrder.splice(index, 0, {
|
|
766
|
-
id: taskId,
|
|
767
|
-
depth
|
|
768
|
-
});
|
|
769
|
-
return {
|
|
770
|
-
tasks: nextTasks,
|
|
771
|
-
renderOrder: nextOrder
|
|
772
|
-
};
|
|
773
|
-
});
|
|
774
|
-
yield* markDirty;
|
|
775
|
-
return taskId;
|
|
776
|
-
});
|
|
777
|
-
const updateTask = (taskId, options) => Ref.update(storeRef, (store) => {
|
|
778
|
-
const snapshot = store.tasks.get(taskId);
|
|
779
|
-
if (!snapshot) return store;
|
|
780
|
-
const nextTasks = new Map(store.tasks);
|
|
781
|
-
const nextSnapshot = updatedSnapshot(snapshot, options);
|
|
782
|
-
nextTasks.set(taskId, nextSnapshot);
|
|
783
|
-
if (options.transient !== void 0) for (const [candidateId, candidate] of store.tasks.entries()) {
|
|
784
|
-
if (candidateId === taskId) continue;
|
|
785
|
-
let parentId = candidate.parentId;
|
|
786
|
-
let isDescendant = false;
|
|
787
|
-
while (parentId !== null) {
|
|
788
|
-
if (parentId === taskId) {
|
|
789
|
-
isDescendant = true;
|
|
790
|
-
break;
|
|
791
|
-
}
|
|
792
|
-
parentId = store.tasks.get(parentId)?.parentId ?? null;
|
|
793
|
-
}
|
|
794
|
-
if (isDescendant) nextTasks.set(candidateId, withTransient(candidate, nextSnapshot.transient));
|
|
795
|
-
}
|
|
796
|
-
return {
|
|
797
|
-
tasks: nextTasks,
|
|
798
|
-
renderOrder: store.renderOrder
|
|
799
|
-
};
|
|
800
|
-
}).pipe(Effect.zipRight(markDirty));
|
|
801
|
-
const advanceTask = (taskId, amount = 1) => Ref.update(storeRef, (store) => {
|
|
802
|
-
const snapshot = store.tasks.get(taskId);
|
|
803
|
-
if (!snapshot) return store;
|
|
804
|
-
const units = snapshot.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
|
|
805
|
-
succeeded: snapshot.units.succeeded + amount,
|
|
806
|
-
failed: snapshot.units.failed,
|
|
807
|
-
total: snapshot.units.total
|
|
808
|
-
}) : new IndeterminateTaskUnits({ spinnerFrame: Math.max(0, snapshot.units.spinnerFrame + amount) });
|
|
809
|
-
const nextTasks = new Map(store.tasks);
|
|
810
|
-
nextTasks.set(taskId, new TaskSnapshot({
|
|
811
|
-
id: snapshot.id,
|
|
812
|
-
parentId: snapshot.parentId,
|
|
813
|
-
description: snapshot.description,
|
|
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,
|
|
842
|
-
transient: snapshot.transient,
|
|
843
|
-
units,
|
|
844
|
-
startedAt: snapshot.startedAt,
|
|
845
|
-
completedAt: snapshot.completedAt
|
|
846
|
-
}));
|
|
847
|
-
return {
|
|
848
|
-
tasks: nextTasks,
|
|
849
|
-
renderOrder: store.renderOrder
|
|
850
|
-
};
|
|
851
|
-
}).pipe(Effect.zipRight(markDirty));
|
|
852
|
-
const completeTask = (taskId) => Effect.gen(function* () {
|
|
853
|
-
const now = yield* Clock.currentTimeMillis;
|
|
854
|
-
yield* Ref.update(storeRef, (store) => {
|
|
855
|
-
const snapshot = store.tasks.get(taskId);
|
|
856
|
-
if (!snapshot) return store;
|
|
857
|
-
const nextTasks = new Map(store.tasks);
|
|
858
|
-
if (snapshot.transient) {
|
|
859
|
-
nextTasks.delete(taskId);
|
|
860
|
-
return {
|
|
861
|
-
tasks: nextTasks,
|
|
862
|
-
renderOrder: removeFromRenderOrder(store.renderOrder, taskId)
|
|
863
|
-
};
|
|
864
|
-
}
|
|
865
|
-
nextTasks.set(taskId, new TaskSnapshot({
|
|
866
|
-
id: snapshot.id,
|
|
867
|
-
parentId: snapshot.parentId,
|
|
868
|
-
description: snapshot.description,
|
|
869
|
-
status: "done",
|
|
870
|
-
countDisplay: snapshot.countDisplay,
|
|
871
|
-
transient: snapshot.transient,
|
|
872
|
-
units: snapshot.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
|
|
873
|
-
succeeded: snapshot.units.total - snapshot.units.failed,
|
|
874
|
-
failed: snapshot.units.failed,
|
|
875
|
-
total: snapshot.units.total
|
|
876
|
-
}) : snapshot.units,
|
|
877
|
-
startedAt: snapshot.startedAt,
|
|
878
|
-
completedAt: now
|
|
879
|
-
}));
|
|
880
|
-
return {
|
|
881
|
-
tasks: nextTasks,
|
|
882
|
-
renderOrder: store.renderOrder
|
|
883
|
-
};
|
|
884
|
-
});
|
|
885
|
-
yield* markDirty;
|
|
886
|
-
});
|
|
887
|
-
const failTask = (taskId) => Effect.gen(function* () {
|
|
888
|
-
const now = yield* Clock.currentTimeMillis;
|
|
889
|
-
yield* Ref.update(storeRef, (store) => {
|
|
890
|
-
const snapshot = store.tasks.get(taskId);
|
|
891
|
-
if (!snapshot) return store;
|
|
892
|
-
const nextTasks = new Map(store.tasks);
|
|
893
|
-
if (snapshot.transient) {
|
|
894
|
-
nextTasks.delete(taskId);
|
|
895
|
-
return {
|
|
896
|
-
tasks: nextTasks,
|
|
897
|
-
renderOrder: removeFromRenderOrder(store.renderOrder, taskId)
|
|
898
|
-
};
|
|
899
|
-
}
|
|
900
|
-
nextTasks.set(taskId, new TaskSnapshot({
|
|
901
|
-
id: snapshot.id,
|
|
902
|
-
parentId: snapshot.parentId,
|
|
903
|
-
description: snapshot.description,
|
|
904
|
-
status: "failed",
|
|
905
|
-
countDisplay: snapshot.countDisplay,
|
|
906
|
-
transient: snapshot.transient,
|
|
907
|
-
units: snapshot.units,
|
|
908
|
-
startedAt: snapshot.startedAt,
|
|
909
|
-
completedAt: now
|
|
910
|
-
}));
|
|
911
|
-
return {
|
|
912
|
-
tasks: nextTasks,
|
|
913
|
-
renderOrder: store.renderOrder
|
|
914
|
-
};
|
|
1463
|
+
return yield* store.addTask({
|
|
1464
|
+
...options,
|
|
1465
|
+
parentId: Option.isSome(resolvedParentId) ? resolvedParentId.value : void 0
|
|
915
1466
|
});
|
|
916
|
-
yield* markDirty;
|
|
917
1467
|
});
|
|
918
|
-
const
|
|
919
|
-
const
|
|
1468
|
+
const updateTask = store.updateTask;
|
|
1469
|
+
const incrementSucceeded = store.incrementSucceeded;
|
|
1470
|
+
const incrementFailed = store.incrementFailed;
|
|
1471
|
+
const completeTask = store.completeTask;
|
|
1472
|
+
const failTask = store.failTask;
|
|
1473
|
+
const getTask = store.getTask;
|
|
1474
|
+
const listTasks = store.listTasks;
|
|
920
1475
|
const runTask = dual(2, (effect, options) => Effect.gen(function* () {
|
|
921
1476
|
const inheritedParentId = yield* FiberRef.get(currentParentRef);
|
|
922
1477
|
const resolvedParentId = options.parentId === void 0 ? inheritedParentId : Option.some(options.parentId);
|
|
@@ -930,8 +1485,8 @@ const makeProgressService = Effect.gen(function* () {
|
|
|
930
1485
|
const service = {
|
|
931
1486
|
addTask,
|
|
932
1487
|
updateTask,
|
|
933
|
-
|
|
934
|
-
|
|
1488
|
+
incrementSucceeded,
|
|
1489
|
+
incrementFailed,
|
|
935
1490
|
completeTask,
|
|
936
1491
|
failTask,
|
|
937
1492
|
log,
|
|
@@ -991,11 +1546,11 @@ const allCountDisplay = (mode) => isCollectAllMode(mode) ? "detailed" : "process
|
|
|
991
1546
|
const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* () {
|
|
992
1547
|
const exit = yield* Effect.exit(effect);
|
|
993
1548
|
if (Exit.isSuccess(exit)) {
|
|
994
|
-
yield* progress.
|
|
1549
|
+
yield* progress.incrementSucceeded(taskId, 1);
|
|
995
1550
|
return exit.value;
|
|
996
1551
|
}
|
|
997
1552
|
if (Cause.isInterruptedOnly(exit.cause)) return yield* Effect.failCause(exit.cause);
|
|
998
|
-
yield* progress.
|
|
1553
|
+
yield* progress.incrementFailed(taskId, 1);
|
|
999
1554
|
return yield* Effect.failCause(exit.cause);
|
|
1000
1555
|
});
|
|
1001
1556
|
const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
|