effective-progress 0.6.1 → 0.7.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 CHANGED
@@ -83,12 +83,15 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
83
83
  - In fail-fast runs, unresolved units remain unprocessed.
84
84
  - `mode: "either"` and `mode: "validate"` run all effects and keep mixed outcomes in the task counters.
85
85
  - Mixed outcomes can still finalize as `done` when all units are accounted for.
86
+ - Empty collections are valid inputs for `Progress.all` / `Progress.forEach` and render as `0/0` instead of failing.
86
87
 
87
88
  ### Other examples
88
89
 
89
90
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
90
91
  - `examples/advancedExample.ts` - full API usage and manual task control
91
92
  - `examples/mixedOutcomes.ts` - fail-fast vs `either`/`validate` with mixed success/failure counters
93
+ - `examples/cliProgressSemantics.ts` - zero totals, negative totals, overflow counts, and empty `all` / `forEach`
94
+ - `examples/unknownTotalCounting.ts` - count successes/failures without a known total and render `processed/?`
92
95
  - `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
93
96
  - `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
94
97
 
@@ -106,6 +109,8 @@ Support for `either`/`validate` modes of `Effect.all` and render the amount of s
106
109
  - Built-in columns are: description, bar, amount/spinner, elapsed, and ETA.
107
110
  - Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
108
111
  - Determinate amount text shows counters without prefixes: `<succeeded> <failed> <processed>/<total>`.
112
+ - Counts can exceed `total`; the amount text keeps those raw values (for example `12/10`) while the bar stays visually clamped at full.
113
+ - `total: 0` is valid for determinate tasks and renders as a full bar by default.
109
114
  - Column widths are shared per frame (widest visible cell wins), so rows stay aligned.
110
115
  - Elapsed and ETA reserve stable widths to reduce jitter while tasks transition states.
111
116
  - Layout uses a 100-column baseline and grows when content requires more space.
@@ -131,6 +136,12 @@ const program = Progress.task(
131
136
  );
132
137
  ```
133
138
 
139
+ Manual total behavior follows cli-progress-style semantics:
140
+
141
+ - negative totals on task creation fall back to `100`
142
+ - negative totals on later `updateTask` calls are ignored
143
+ - explicit `total: undefined` on `updateTask` clears the total and switches back to indeterminate rendering
144
+
134
145
  ## Column customization
135
146
 
136
147
  Custom column APIs are not part of the first Ink release. The renderer ships with built-in columns only, and old renderer config APIs (`RendererConfig`, `ProgressBarConfig`, custom column definitions) are intentionally removed in this iteration.
package/dist/index.d.mts CHANGED
@@ -26,20 +26,12 @@ interface UpdateTaskOptions {
26
26
  readonly countDisplay?: TaskCountDisplay;
27
27
  }
28
28
  type TrackOptions = Exclude<AddTaskOptions, "parentId">;
29
- declare const DeterminateTaskUnits_base: Schema.TaggedClass<DeterminateTaskUnits, "DeterminateTaskUnits", {
30
- readonly _tag: Schema.tag<"DeterminateTaskUnits">;
31
- } & {
29
+ declare const TaskUnitsSchema: Schema.Struct<{
32
30
  succeeded: typeof Schema.Number;
33
31
  failed: typeof Schema.Number;
34
32
  processed: typeof Schema.Number;
35
- total: typeof Schema.Number;
36
- }>;
37
- declare class DeterminateTaskUnits extends DeterminateTaskUnits_base {}
38
- declare const IndeterminateTaskUnits_base: Schema.TaggedClass<IndeterminateTaskUnits, "IndeterminateTaskUnits", {
39
- readonly _tag: Schema.tag<"IndeterminateTaskUnits">;
33
+ total: Schema.optional<typeof Schema.Number>;
40
34
  }>;
41
- declare class IndeterminateTaskUnits extends IndeterminateTaskUnits_base {}
42
- declare const TaskUnitsSchema: Schema.Union<[typeof DeterminateTaskUnits, typeof IndeterminateTaskUnits]>;
43
35
  type TaskUnits = typeof TaskUnitsSchema.Type;
44
36
  declare const TaskSnapshot_base: Schema.TaggedClass<TaskSnapshot, "TaskSnapshot", {
45
37
  readonly _tag: Schema.tag<"TaskSnapshot">;
@@ -50,7 +42,12 @@ declare const TaskSnapshot_base: Schema.TaggedClass<TaskSnapshot, "TaskSnapshot"
50
42
  status: Schema.Literal<["running", "done", "failed"]>;
51
43
  countDisplay: Schema.Literal<["processedOnly", "detailed"]>;
52
44
  transient: typeof Schema.Boolean;
53
- units: Schema.Union<[typeof DeterminateTaskUnits, typeof IndeterminateTaskUnits]>;
45
+ units: Schema.Struct<{
46
+ succeeded: typeof Schema.Number;
47
+ failed: typeof Schema.Number;
48
+ processed: typeof Schema.Number;
49
+ total: Schema.optional<typeof Schema.Number>;
50
+ }>;
54
51
  startedAt: typeof Schema.Number;
55
52
  completedAt: Schema.NullOr<typeof Schema.Number>;
56
53
  }>;
@@ -185,4 +182,4 @@ declare class ProgressStdio extends ProgressStdio_base {
185
182
  static readonly Default: Layer.Layer<ProgressStdio, never, never>;
186
183
  }
187
184
  //#endregion
188
- export { AddTaskOptions, AllOptions, AllReturn, DeterminateTaskUnits, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, IndeterminateTaskUnits, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
185
+ export { AddTaskOptions, AllOptions, AllReturn, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskOptions, TaskRemovedEvent, TaskSnapshot, TaskStatus, TaskStatusSchema, TaskStore, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task };
package/dist/index.mjs CHANGED
@@ -10,14 +10,12 @@ const TaskIdSchema = Schema.Number.pipe(Schema.brand("TaskId"));
10
10
  const TaskId = Brand.nominal();
11
11
  const TaskStatusSchema = Schema.Literal("running", "done", "failed");
12
12
  const TaskCountDisplaySchema = Schema.Literal("processedOnly", "detailed");
13
- var DeterminateTaskUnits = class extends Schema.TaggedClass()("DeterminateTaskUnits", {
13
+ const TaskUnitsSchema = Schema.Struct({
14
14
  succeeded: Schema.Number,
15
15
  failed: Schema.Number,
16
16
  processed: Schema.Number,
17
- total: Schema.Number
18
- }) {};
19
- var IndeterminateTaskUnits = class extends Schema.TaggedClass()("IndeterminateTaskUnits", {}) {};
20
- const TaskUnitsSchema = Schema.Union(DeterminateTaskUnits, IndeterminateTaskUnits);
17
+ total: Schema.optional(Schema.Number)
18
+ });
21
19
  var TaskSnapshot = class extends Schema.TaggedClass()("TaskSnapshot", {
22
20
  id: TaskIdSchema,
23
21
  parentId: Schema.NullOr(TaskIdSchema),
@@ -114,40 +112,37 @@ const toRenderSnapshot = (store) => {
114
112
 
115
113
  //#endregion
116
114
  //#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({
115
+ const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
116
+ const DEFAULT_TOTAL = 100;
117
+ const sanitizeTotalOnAdd = (total) => {
118
+ if (total === void 0) return;
119
+ return total < 0 ? DEFAULT_TOTAL : total;
120
+ };
121
+ const sanitizeTotalOnUpdate = (currentTotal, nextTotal) => {
122
+ if (nextTotal === void 0) return;
123
+ return nextTotal < 0 ? currentTotal : nextTotal;
124
+ };
125
+ const normalizeUnits = (counts) => {
126
+ const succeeded = Math.max(0, counts.succeeded);
127
+ const failed = Math.max(0, counts.failed);
128
+ return counts.total === void 0 ? {
129
+ succeeded,
130
+ failed,
131
+ processed: succeeded + failed
132
+ } : {
122
133
  succeeded,
123
134
  failed,
124
135
  processed: succeeded + failed,
125
- total
126
- });
136
+ total: counts.total
137
+ };
127
138
  };
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
139
  const updatedSnapshot = (snapshot, options) => {
134
140
  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
- })();
141
+ const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
142
+ succeeded: options.succeeded ?? currentUnits.succeeded,
143
+ failed: options.failed ?? currentUnits.failed,
144
+ total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(currentUnits.total, options.total) : currentUnits.total
145
+ });
151
146
  return new TaskSnapshot({
152
147
  id: snapshot.id,
153
148
  parentId: snapshot.parentId,
@@ -265,10 +260,10 @@ const makeProgressRenderStore = () => {
265
260
  },
266
261
  addTask: (options) => Effect.gen(function* () {
267
262
  const taskId = TaskId(++nextTaskId);
268
- const units = options.total === void 0 || options.total <= 0 ? new IndeterminateTaskUnits({}) : normalizeDeterminateCounts({
263
+ const units = normalizeUnits({
269
264
  succeeded: 0,
270
265
  failed: 0,
271
- total: options.total
266
+ total: sanitizeTotalOnAdd(options.total)
272
267
  });
273
268
  const parentSnapshot = options.parentId === void 0 ? void 0 : state.tasks.get(options.parentId);
274
269
  const now = yield* Clock.currentTimeMillis;
@@ -301,12 +296,13 @@ const makeProgressRenderStore = () => {
301
296
  });
302
297
  return taskId;
303
298
  }),
304
- updateTask: (taskId, options) => Effect.sync(() => {
299
+ updateTask: (taskId, options) => Effect.gen(function* () {
300
+ const currentTask = state.tasks.get(taskId);
301
+ if (!currentTask) return;
302
+ const nextTask = updatedSnapshot(currentTask, options);
305
303
  updateState((current) => {
306
- const currentTask = current.tasks.get(taskId);
307
- if (!currentTask) return current;
304
+ if (!current.tasks.get(taskId)) return current;
308
305
  const nextTasks = new Map(current.tasks);
309
- const nextTask = updatedSnapshot(currentTask, options);
310
306
  nextTasks.set(taskId, nextTask);
311
307
  if (options.transient !== void 0) for (const [candidateId, candidate] of current.tasks.entries()) {
312
308
  if (candidateId === taskId) continue;
@@ -330,7 +326,7 @@ const makeProgressRenderStore = () => {
330
326
  incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
331
327
  updateState((current) => {
332
328
  const currentTask = current.tasks.get(taskId);
333
- if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
329
+ if (!currentTask) return current;
334
330
  const nextTasks = new Map(current.tasks);
335
331
  nextTasks.set(taskId, new TaskSnapshot({
336
332
  id: currentTask.id,
@@ -339,7 +335,7 @@ const makeProgressRenderStore = () => {
339
335
  status: currentTask.status,
340
336
  countDisplay: currentTask.countDisplay,
341
337
  transient: currentTask.transient,
342
- units: normalizeDeterminateCounts({
338
+ units: normalizeUnits({
343
339
  succeeded: currentTask.units.succeeded + amount,
344
340
  failed: currentTask.units.failed,
345
341
  total: currentTask.units.total
@@ -356,7 +352,7 @@ const makeProgressRenderStore = () => {
356
352
  incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
357
353
  updateState((current) => {
358
354
  const currentTask = current.tasks.get(taskId);
359
- if (!currentTask || currentTask.units._tag !== "DeterminateTaskUnits") return current;
355
+ if (!currentTask) return current;
360
356
  const nextTasks = new Map(current.tasks);
361
357
  nextTasks.set(taskId, new TaskSnapshot({
362
358
  id: currentTask.id,
@@ -365,7 +361,7 @@ const makeProgressRenderStore = () => {
365
361
  status: currentTask.status,
366
362
  countDisplay: currentTask.countDisplay,
367
363
  transient: currentTask.transient,
368
- units: normalizeDeterminateCounts({
364
+ units: normalizeUnits({
369
365
  succeeded: currentTask.units.succeeded,
370
366
  failed: currentTask.units.failed + amount,
371
367
  total: currentTask.units.total
@@ -399,10 +395,14 @@ const makeProgressRenderStore = () => {
399
395
  status: "done",
400
396
  countDisplay: currentTask.countDisplay,
401
397
  transient: currentTask.transient,
402
- units: currentTask.units._tag === "DeterminateTaskUnits" ? normalizeDeterminateCounts({
403
- succeeded: currentTask.units.total - currentTask.units.failed,
398
+ units: currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
399
+ succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
404
400
  failed: currentTask.units.failed,
405
401
  total: currentTask.units.total
402
+ }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
403
+ succeeded: currentTask.units.succeeded,
404
+ failed: currentTask.units.failed,
405
+ total: currentTask.units.processed
406
406
  }) : currentTask.units,
407
407
  startedAt: currentTask.startedAt,
408
408
  completedAt: now
@@ -462,6 +462,8 @@ const SPINNER_FRAMES = [
462
462
  "⠇",
463
463
  "⠏"
464
464
  ];
465
+ const isDeterminate$1 = (task) => task.units.total !== void 0;
466
+ const showsUnknownTotalCounts = (task) => task.units.total === void 0 && task.units.processed > 0;
465
467
  const formatDurationSeconds = (seconds) => {
466
468
  const value = Math.max(0, Math.floor(seconds));
467
469
  if (value < 60) return `${value}s`;
@@ -478,7 +480,7 @@ const formatElapsed = (task, now) => {
478
480
  return formatDurationSeconds(Math.max(0, (task.completedAt ?? now) - task.startedAt) / 1e3);
479
481
  };
480
482
  const formatEta = (task, now) => {
481
- if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") return "";
483
+ if (task.status !== "running" || !isDeterminate$1(task)) return "";
482
484
  const { processed, total } = task.units;
483
485
  const remaining = total - processed;
484
486
  if (processed <= 0 || remaining <= 0) return "";
@@ -494,7 +496,7 @@ const getTaskIndicator = (task, tick) => {
494
496
  symbol: "✗",
495
497
  color: "red"
496
498
  };
497
- if (task.units._tag !== "DeterminateTaskUnits") return {
499
+ if (!isDeterminate$1(task)) return {
498
500
  symbol: "✓",
499
501
  color: "green"
500
502
  };
@@ -517,7 +519,7 @@ const getTaskIndicator = (task, tick) => {
517
519
  };
518
520
  };
519
521
  const formatDeterminateAmountParts = (task) => {
520
- if (task.units._tag !== "DeterminateTaskUnits") return;
522
+ if (!isDeterminate$1(task)) return;
521
523
  const totalText = `${task.units.total}`;
522
524
  const width = totalText.length;
523
525
  const processedText = `${task.units.processed}`;
@@ -529,19 +531,23 @@ const formatDeterminateAmountParts = (task) => {
529
531
  };
530
532
  };
531
533
  const formatAmount = (task, _tick) => {
532
- if (task.units._tag === "DeterminateTaskUnits") {
534
+ if (isDeterminate$1(task)) {
533
535
  const parts = formatDeterminateAmountParts(task);
534
536
  if (parts === void 0) return "";
535
537
  if (task.countDisplay === "detailed") return `${parts.succeeded} ${parts.failed} ${parts.processed}/${parts.total}`;
536
538
  return `${parts.processed}/${parts.total}`;
537
539
  }
538
- if (task.status === "running" && task.units._tag === "IndeterminateTaskUnits") return "";
540
+ if (showsUnknownTotalCounts(task)) {
541
+ if (task.countDisplay === "detailed") return `${task.units.succeeded} ${task.units.failed} ${task.units.processed}/?`;
542
+ return `${task.units.processed}/?`;
543
+ }
544
+ if (task.status === "running") return "";
539
545
  return task.status === "failed" ? "✗" : "";
540
546
  };
541
547
 
542
548
  //#endregion
543
549
  //#region src/ink-renderer/columns/determinate.ts
544
- const isDeterminate = (task) => task.units._tag === "DeterminateTaskUnits";
550
+ const isDeterminate = (task) => task.units.total !== void 0;
545
551
  const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
546
552
 
547
553
  //#endregion
@@ -578,10 +584,11 @@ const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.v
578
584
  const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
579
585
  const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
580
586
  const blank = (width) => " ".repeat(Math.max(0, width));
581
- const shouldShowDetailedCounts = (task) => task.units._tag === "DeterminateTaskUnits" && task.countDisplay === "detailed";
587
+ const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
588
+ const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
582
589
  const SucceededCountColumn = ({ task, width }) => {
583
590
  if (width <= 0) return null;
584
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
591
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
585
592
  if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
586
593
  return /* @__PURE__ */ jsx(Text, {
587
594
  color: "green",
@@ -590,7 +597,7 @@ const SucceededCountColumn = ({ task, width }) => {
590
597
  };
591
598
  const FailedCountColumn = ({ task, width }) => {
592
599
  if (width <= 0) return null;
593
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
600
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
594
601
  if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
595
602
  return /* @__PURE__ */ jsx(Text, {
596
603
  color: "red",
@@ -599,11 +606,11 @@ const FailedCountColumn = ({ task, width }) => {
599
606
  };
600
607
  const ProcessedCountColumn = ({ task, width }) => {
601
608
  if (width <= 0) return null;
602
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
609
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
603
610
  return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
604
611
  };
605
612
  const AmountSeparatorColumn = ({ task, tick }) => {
606
- if (task.units._tag === "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: "/" });
613
+ if (shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: "/" });
607
614
  const symbol = formatAmount(task, tick);
608
615
  if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
609
616
  color: "red",
@@ -617,8 +624,8 @@ const AmountSeparatorColumn = ({ task, tick }) => {
617
624
  };
618
625
  const TotalCountColumn = ({ task, width }) => {
619
626
  if (width <= 0) return null;
620
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, { children: blank(width) });
621
- return /* @__PURE__ */ jsx(Text, { children: padRight(`${task.units.total}`, width) });
627
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
628
+ return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
622
629
  };
623
630
  const AmountColumn = ({ task, tick, layout }) => {
624
631
  if (layout.kind === "text") {
@@ -682,44 +689,47 @@ const AmountColumn = ({ task, tick, layout }) => {
682
689
  });
683
690
  };
684
691
  const computeAmountMetrics = (rows, tick) => {
685
- let hasDeterminate = false;
692
+ let hasStructuredCounts = false;
686
693
  let hasDetailed = false;
687
- let totalDigits = 0;
694
+ let countDigits = 0;
695
+ let totalWidth = 0;
688
696
  let simpleTextWidth = 0;
689
697
  for (const row of rows) {
690
698
  const { task } = row;
691
- if (isDeterminate(task)) {
692
- hasDeterminate = true;
693
- totalDigits = Math.max(totalDigits, textWidth(`${task.units.total}`));
699
+ if (shouldShowCountAmount(task)) {
700
+ hasStructuredCounts = true;
701
+ countDigits = Math.max(countDigits, textWidth(`${task.units.succeeded}`), textWidth(`${task.units.failed}`), textWidth(`${task.units.processed}`));
702
+ totalWidth = Math.max(totalWidth, textWidth(isDeterminate(task) ? `${task.units.total}` : "?"));
694
703
  if (task.countDisplay === "detailed") hasDetailed = true;
695
704
  continue;
696
705
  }
697
706
  simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
698
707
  }
699
708
  return {
700
- hasDeterminate,
709
+ hasStructuredCounts,
701
710
  hasDetailed,
702
- totalDigits: Math.max(1, totalDigits),
711
+ countDigits: Math.max(1, countDigits),
712
+ totalWidth: Math.max(1, totalWidth),
703
713
  simpleTextWidth
704
714
  };
705
715
  };
706
716
  const detailedAmountLayout = (metrics) => ({
707
717
  kind: "detailed",
708
- succeededWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
709
- failedWidth: metrics.hasDetailed ? metrics.totalDigits : 0,
710
- processedWidth: metrics.totalDigits,
711
- totalWidth: metrics.totalDigits
718
+ succeededWidth: metrics.hasDetailed ? metrics.countDigits : 0,
719
+ failedWidth: metrics.hasDetailed ? metrics.countDigits : 0,
720
+ processedWidth: metrics.countDigits,
721
+ totalWidth: metrics.totalWidth
712
722
  });
713
723
  const processedAmountLayout = (metrics) => ({
714
724
  kind: "processed",
715
- processedWidth: metrics.totalDigits,
716
- totalWidth: metrics.totalDigits
725
+ processedWidth: metrics.countDigits,
726
+ totalWidth: metrics.totalWidth
717
727
  });
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;
728
+ const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
729
+ const processedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth;
720
730
  const createAmountColumnSpec = (context) => {
721
731
  const metrics = computeAmountMetrics(context.rows, context.tick);
722
- if (!metrics.hasDeterminate && metrics.simpleTextWidth <= 0) return;
732
+ if (!metrics.hasStructuredCounts && metrics.simpleTextWidth <= 0) return;
723
733
  const detailedLayout = detailedAmountLayout(metrics);
724
734
  const processedLayout = processedAmountLayout(metrics);
725
735
  const detailedWidth = detailedAmountWidth(metrics);
@@ -728,7 +738,7 @@ const createAmountColumnSpec = (context) => {
728
738
  id: "amount",
729
739
  grow: 0,
730
740
  canHide: true,
731
- variants: metrics.hasDeterminate && metrics.hasDetailed ? [{
741
+ variants: metrics.hasStructuredCounts && metrics.hasDetailed ? [{
732
742
  id: "detailed",
733
743
  minWidth: detailedWidth,
734
744
  idealWidth: detailedWidth,
@@ -746,7 +756,7 @@ const createAmountColumnSpec = (context) => {
746
756
  tick: context.tick,
747
757
  layout: processedLayout
748
758
  })
749
- }] : metrics.hasDeterminate ? [{
759
+ }] : metrics.hasStructuredCounts ? [{
750
760
  id: "processed",
751
761
  minWidth: processedWidth,
752
762
  idealWidth: processedWidth,
@@ -773,13 +783,14 @@ const createAmountColumnSpec = (context) => {
773
783
  const DEFAULT_BAR_WIDTH = 30;
774
784
  const MIN_BAR_WIDTH = 8;
775
785
  const segmentLengths = (width, total, succeeded, failed) => {
776
- if (total <= 0) return {
777
- succeeded: 0,
786
+ if (total === 0) return {
787
+ succeeded: width,
778
788
  failed: 0,
779
- remaining: width
789
+ remaining: 0
780
790
  };
781
- const succeededEnd = Math.round(succeeded / total * width);
782
- const failedEnd = Math.round((succeeded + failed) / total * width);
791
+ const displayTotal = Math.max(total, succeeded + failed);
792
+ const succeededEnd = Math.round(succeeded / displayTotal * width);
793
+ const failedEnd = Math.round((succeeded + failed) / displayTotal * width);
783
794
  const succeededLength = Math.max(0, Math.min(width, succeededEnd));
784
795
  const failedLength = Math.max(0, Math.min(width, failedEnd) - succeededLength);
785
796
  return {
@@ -789,7 +800,7 @@ const segmentLengths = (width, total, succeeded, failed) => {
789
800
  };
790
801
  };
791
802
  const BarColumn = ({ task, width }) => {
792
- if (task.units._tag !== "DeterminateTaskUnits") return /* @__PURE__ */ jsx(Text, {});
803
+ if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, {});
793
804
  const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
794
805
  return /* @__PURE__ */ jsxs(Text, {
795
806
  wrap: "truncate-end",
@@ -1555,9 +1566,9 @@ const wrapTrackedEffect = (progress, taskId, effect) => Effect.gen(function* ()
1555
1566
  });
1556
1567
  const isTaskFullyProcessed = (progress, taskId) => Effect.gen(function* () {
1557
1568
  const taskOption = yield* progress.getTask(taskId);
1558
- if (Option.isNone(taskOption) || taskOption.value.units._tag !== "DeterminateTaskUnits") return false;
1569
+ if (Option.isNone(taskOption)) return false;
1559
1570
  const { processed, total } = taskOption.value.units;
1560
- return processed >= total;
1571
+ return total !== void 0 && processed >= total;
1561
1572
  });
1562
1573
  const all = dual(2, (effects, options) => provideProgress(Effect.gen(function* () {
1563
1574
  const progress = yield* Progress;
@@ -1610,4 +1621,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1610
1621
  })));
1611
1622
 
1612
1623
  //#endregion
1613
- export { DeterminateTaskUnits, IndeterminateTaskUnits, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1624
+ export { Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "description": "Effect-first terminal progress bars with nested multibar support",
5
5
  "homepage": "https://github.com/stromseng/effective-progress#readme",
6
6
  "bugs": {