effective-progress 0.11.0 → 0.11.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 CHANGED
@@ -6,6 +6,8 @@
6
6
  > Pre-`1.0.0`, breaking changes may happen in any minor release. SemVer guarantees will begin at `1.0.0`.
7
7
  > I recommend using only the `Progress.all` and `Progress.forEach` APIs for now, as they will likely change the least. The lower-level APIs for manual progress bar control are more likely to see breaking changes as I iterate on the design.
8
8
  >
9
+ > I am currently waiting on https://github.com/anomalyco/opentui/issues/204 to swap the renderer to opentui.
10
+ >
9
11
  > Please open an issue or reach out if you have any questions or want to contribute!
10
12
  > Feedback and contributions are very welcome!
11
13
 
@@ -145,8 +147,10 @@ Effect.runPromise(program);
145
147
  ### Ink renderer behavior
146
148
 
147
149
  - Rendering is powered by [Ink](https://github.com/vadimdemedes/ink).
148
- - Built-in columns are exposed as `Progress.Columns.description()`, `bar()`, `amount()`, `elapsed()`, `eta()`, `spacer()`, and `defaults()`.
150
+ - Built-in columns are exposed as `Progress.Columns.description()`, `bar()`, `amount()`, `elapsedEta()`, `elapsed()`, `eta()`, `spacer()`, and `defaults()`.
151
+ - `elapsedEta()` renders a compact clock-style column as `elapsed<eta` using the shape `00:00<00:00`; `defaults()` now uses that combined column.
149
152
  - Determinate bars are segmented by outcome: succeeded (green), failed (red), and remaining (neutral).
153
+ - `bar()` defaults to a fixed width of `30`; pass `bar({ size: "fullwidth" })` to consume remaining row width or `bar({ size: 12 })` for an explicit width.
150
154
  - Determinate amount text shows counters without prefixes: `<succeeded> <failed> <processed>/<total>`.
151
155
  - Counts can exceed `total`; the amount text keeps those raw values (for example `12/10`) while the bar stays visually clamped at full.
152
156
  - `total: 0` is valid for determinate tasks and renders as a full bar by default.
package/dist/index.d.mts CHANGED
@@ -108,6 +108,11 @@ declare const TaskUnitsSchema: Schema.Struct<{
108
108
  total: Schema.optional<typeof Schema.Number>;
109
109
  }>;
110
110
  type TaskUnits = typeof TaskUnitsSchema.Type;
111
+ declare const TaskProgressSampleSchema: Schema.Struct<{
112
+ timestamp: typeof Schema.Number;
113
+ processed: typeof Schema.Number;
114
+ }>;
115
+ type TaskProgressSample = typeof TaskProgressSampleSchema.Type;
111
116
  declare const TaskSnapshotSchema: Schema.Struct<{
112
117
  id: Schema.brand<typeof Schema.Number, "TaskId">;
113
118
  parentId: Schema.NullOr<Schema.brand<typeof Schema.Number, "TaskId">>;
@@ -123,6 +128,10 @@ declare const TaskSnapshotSchema: Schema.Struct<{
123
128
  }>;
124
129
  startedAt: typeof Schema.Number;
125
130
  completedAt: Schema.NullOr<typeof Schema.Number>;
131
+ progressSamples: Schema.Array$<Schema.Struct<{
132
+ timestamp: typeof Schema.Number;
133
+ processed: typeof Schema.Number;
134
+ }>>;
126
135
  metadata: typeof Schema.Unknown;
127
136
  }>;
128
137
  type TaskSnapshot = typeof TaskSnapshotSchema.Type;
@@ -296,9 +305,10 @@ interface BarPrepared {
296
305
  //#region src/renderer/columns/description-column.d.ts
297
306
  interface DescriptionPrepared {
298
307
  readonly minTreeWidth: number;
308
+ readonly preferredWidth: number;
299
309
  }
300
310
  declare namespace columns_d_exports {
301
- export { AmountLayout, BarPrepared, DescriptionPrepared, SpacerOptions, amount, bar, defaults, description, elapsed, eta, resolveColumnSizeValue, spacer };
311
+ export { AmountLayout, BarOptions, BarPrepared, DescriptionPrepared, SpacerOptions, amount, bar, defaults, description, elapsed, elapsedEta, eta, resolveColumnSizeValue, spacer };
302
312
  }
303
313
  interface SpacerOptions {
304
314
  readonly flexGrow?: number;
@@ -306,6 +316,9 @@ interface SpacerOptions {
306
316
  readonly flexBasis?: number;
307
317
  readonly minWidth?: number;
308
318
  }
319
+ interface BarOptions {
320
+ readonly size?: number | "fullwidth";
321
+ }
309
322
  declare const spacer: <M = unknown>({
310
323
  flexGrow,
311
324
  flexShrink,
@@ -313,9 +326,12 @@ declare const spacer: <M = unknown>({
313
326
  minWidth
314
327
  }?: SpacerOptions) => ColumnDef<M>;
315
328
  declare const description: () => ColumnDef<any, DescriptionPrepared>;
316
- declare const bar: () => ColumnDef<any, BarPrepared>;
329
+ declare const bar: ({
330
+ size
331
+ }?: BarOptions) => ColumnDef<any, BarPrepared>;
317
332
  declare const amount: () => ColumnDef<any, AmountLayout>;
318
333
  declare const elapsed: () => ColumnDef<any>;
334
+ declare const elapsedEta: () => ColumnDef<any>;
319
335
  declare const eta: () => ColumnDef<any>;
320
336
  declare const defaults: () => ReadonlyArray<ColumnDef<any, any>>;
321
337
  declare const resolveColumnSizeValue: <P>(value: ColumnSizeValue<P> | undefined, prepared: P) => number | undefined;
@@ -330,4 +346,4 @@ declare class ProgressStdio extends ProgressStdio_base {
330
346
  static readonly Default: Layer.Layer<ProgressStdio, never, never>;
331
347
  }
332
348
  //#endregion
333
- export { AddTaskOptions, AllOptions, AllReturn, CellInfo, ColumnAlign, ColumnDef, ColumnRenderContext, ColumnSizeValue, columns_d_exports as Columns, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskHandle, TaskId, TaskOptions, TaskRemovedEvent, TaskRowDerived, TaskSnapshot, TaskSnapshotSchema, TaskStatus, TaskStatusSchema, TaskStore, TaskTreeInfo, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task$1 as task };
349
+ export { AddTaskOptions, AllOptions, AllReturn, CellInfo, ColumnAlign, ColumnDef, ColumnRenderContext, ColumnSizeValue, columns_d_exports as Columns, EffectAllExecutionOptions, EffectExecutionOptions, ForEachExecutionOptions, ForEachOptions, Progress, ProgressService, ProgressStdio, ProgressStdioService, ProgressTaskEvent, ProgressTaskEventSchema, RenderRow, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplay, TaskCountDisplaySchema, TaskFailedEvent, TaskHandle, TaskId, TaskOptions, TaskProgressSample, TaskProgressSampleSchema, TaskRemovedEvent, TaskRowDerived, TaskSnapshot, TaskSnapshotSchema, TaskStatus, TaskStatusSchema, TaskStore, TaskTreeInfo, TaskUnits, TaskUnitsSchema, TaskUpdatedEvent, TrackOptions, UpdateTaskOptions, all, decodeProgressTaskEvent, forEach, task$1 as task };
package/dist/index.mjs CHANGED
@@ -18,6 +18,10 @@ const TaskUnitsSchema = Schema.Struct({
18
18
  processed: Schema.Number,
19
19
  total: Schema.optional(Schema.Number)
20
20
  });
21
+ const TaskProgressSampleSchema = Schema.Struct({
22
+ timestamp: Schema.Number,
23
+ processed: Schema.Number
24
+ });
21
25
  const TaskSnapshotSchema = Schema.Struct({
22
26
  id: TaskIdSchema,
23
27
  parentId: Schema.NullOr(TaskIdSchema),
@@ -28,6 +32,7 @@ const TaskSnapshotSchema = Schema.Struct({
28
32
  units: TaskUnitsSchema,
29
33
  startedAt: Schema.Number,
30
34
  completedAt: Schema.NullOr(Schema.Number),
35
+ progressSamples: Schema.Array(TaskProgressSampleSchema),
31
36
  metadata: Schema.Unknown
32
37
  });
33
38
  const TaskSnapshot = (snapshot) => snapshot;
@@ -63,6 +68,8 @@ const decodeProgressTaskEvent = Schema.decodeUnknownSync(ProgressTaskEventSchema
63
68
 
64
69
  //#endregion
65
70
  //#region src/renderer/store.ts
71
+ const ETA_SAMPLE_WINDOW_MILLIS = 3e4;
72
+ const ETA_SAMPLE_MAX_LENGTH = 1e3;
66
73
  const hasExplicitTotal = (options) => Object.prototype.hasOwnProperty.call(options, "total");
67
74
  const sanitizeTotalOnAdd = (total) => {
68
75
  if (total === void 0) return;
@@ -86,7 +93,25 @@ const normalizeUnits = (counts) => {
86
93
  total: counts.total
87
94
  };
88
95
  };
89
- const updatedSnapshot = (snapshot, options) => {
96
+ /**
97
+ * Returns a new progress sample deque with the latest processed count appended.
98
+ *
99
+ * Samples are retained for the ETA rolling window and capped by count so very chatty tasks do not
100
+ * grow memory without bound.
101
+ */
102
+ const appendProgressSample = (samples, now, processed) => {
103
+ const previousSamples = samples ?? [];
104
+ if (previousSamples.at(-1)?.processed === processed) return previousSamples;
105
+ const windowStart = now - ETA_SAMPLE_WINDOW_MILLIS;
106
+ const nextSamples = [...previousSamples, {
107
+ timestamp: now,
108
+ processed
109
+ }];
110
+ while (nextSamples.length > 2 && (nextSamples.length > ETA_SAMPLE_MAX_LENGTH || nextSamples[1].timestamp < windowStart)) nextSamples.shift();
111
+ return nextSamples;
112
+ };
113
+ /** Applies mutable task fields and records a progress sample when the processed count changes. */
114
+ const updatedSnapshot = (snapshot, options, now) => {
90
115
  const currentUnits = snapshot.units;
91
116
  const units = options.succeeded === void 0 && options.failed === void 0 && options.total === void 0 && !hasExplicitTotal(options) ? currentUnits : normalizeUnits({
92
117
  succeeded: options.succeeded ?? currentUnits.succeeded,
@@ -94,30 +119,14 @@ const updatedSnapshot = (snapshot, options) => {
94
119
  total: hasExplicitTotal(options) ? sanitizeTotalOnUpdate(options.total) : currentUnits.total
95
120
  });
96
121
  return TaskSnapshot({
97
- id: snapshot.id,
98
- parentId: snapshot.parentId,
122
+ ...snapshot,
99
123
  description: options.description ?? snapshot.description,
100
- status: snapshot.status,
101
124
  countDisplay: options.countDisplay ?? snapshot.countDisplay,
102
125
  transient: options.transient ?? snapshot.transient,
103
126
  units,
104
- startedAt: snapshot.startedAt,
105
- completedAt: snapshot.completedAt,
106
- metadata: snapshot.metadata
127
+ progressSamples: appendProgressSample(snapshot.progressSamples, now, units.processed)
107
128
  });
108
129
  };
109
- const withTransient = (snapshot, transient) => TaskSnapshot({
110
- id: snapshot.id,
111
- parentId: snapshot.parentId,
112
- description: snapshot.description,
113
- status: snapshot.status,
114
- countDisplay: snapshot.countDisplay,
115
- transient,
116
- units: snapshot.units,
117
- startedAt: snapshot.startedAt,
118
- completedAt: snapshot.completedAt,
119
- metadata: snapshot.metadata
120
- });
121
130
  const findInsertionIndex = (renderOrder, parentId) => {
122
131
  if (parentId === null) return {
123
132
  index: renderOrder.length,
@@ -250,6 +259,10 @@ const makeProgressRenderStore = () => {
250
259
  units,
251
260
  startedAt: now,
252
261
  completedAt: null,
262
+ progressSamples: [{
263
+ timestamp: now,
264
+ processed: units.processed
265
+ }],
253
266
  metadata: options.metadata
254
267
  });
255
268
  updateState((current) => {
@@ -279,14 +292,15 @@ const makeProgressRenderStore = () => {
279
292
  });
280
293
  return taskId;
281
294
  }),
282
- updateTask: (taskId, options) => Effect.sync(() => {
295
+ updateTask: (taskId, options) => Effect.gen(function* () {
296
+ const now = yield* Clock.currentTimeMillis;
283
297
  updateState((current) => {
284
298
  const currentTask = current.tasks.get(taskId);
285
299
  if (!currentTask) return {
286
300
  state: current,
287
301
  events: []
288
302
  };
289
- const nextTask = updatedSnapshot(currentTask, options);
303
+ const nextTask = updatedSnapshot(currentTask, options, now);
290
304
  const nextTasks = new Map(current.tasks);
291
305
  nextTasks.set(taskId, nextTask);
292
306
  const events = [new TaskUpdatedEvent({
@@ -302,7 +316,10 @@ const makeProgressRenderStore = () => {
302
316
  if (options.transient !== void 0) for (const candidateId of subtreeTaskIds(current.renderOrder, taskId).slice(1)) {
303
317
  const candidate = current.tasks.get(candidateId);
304
318
  if (!candidate) continue;
305
- const nextCandidate = withTransient(candidate, nextTask.transient);
319
+ const nextCandidate = TaskSnapshot({
320
+ ...candidate,
321
+ transient: nextTask.transient
322
+ });
306
323
  nextTasks.set(candidateId, nextCandidate);
307
324
  events.push(new TaskUpdatedEvent({
308
325
  taskId: candidateId,
@@ -319,29 +336,24 @@ const makeProgressRenderStore = () => {
319
336
  };
320
337
  });
321
338
  }),
322
- incrementSucceeded: (taskId, amount = 1) => Effect.sync(() => {
339
+ incrementSucceeded: (taskId, amount = 1) => Effect.gen(function* () {
340
+ const now = yield* Clock.currentTimeMillis;
323
341
  updateState((current) => {
324
342
  const currentTask = current.tasks.get(taskId);
325
343
  if (!currentTask) return {
326
344
  state: current,
327
345
  events: []
328
346
  };
347
+ const units = normalizeUnits({
348
+ succeeded: currentTask.units.succeeded + amount,
349
+ failed: currentTask.units.failed,
350
+ total: currentTask.units.total
351
+ });
329
352
  const nextTasks = new Map(current.tasks);
330
353
  nextTasks.set(taskId, TaskSnapshot({
331
- id: currentTask.id,
332
- parentId: currentTask.parentId,
333
- description: currentTask.description,
334
- status: currentTask.status,
335
- countDisplay: currentTask.countDisplay,
336
- transient: currentTask.transient,
337
- units: normalizeUnits({
338
- succeeded: currentTask.units.succeeded + amount,
339
- failed: currentTask.units.failed,
340
- total: currentTask.units.total
341
- }),
342
- startedAt: currentTask.startedAt,
343
- completedAt: currentTask.completedAt,
344
- metadata: currentTask.metadata
354
+ ...currentTask,
355
+ units,
356
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
345
357
  }));
346
358
  return {
347
359
  state: {
@@ -357,29 +369,24 @@ const makeProgressRenderStore = () => {
357
369
  };
358
370
  });
359
371
  }),
360
- incrementFailed: (taskId, amount = 1) => Effect.sync(() => {
372
+ incrementFailed: (taskId, amount = 1) => Effect.gen(function* () {
373
+ const now = yield* Clock.currentTimeMillis;
361
374
  updateState((current) => {
362
375
  const currentTask = current.tasks.get(taskId);
363
376
  if (!currentTask) return {
364
377
  state: current,
365
378
  events: []
366
379
  };
380
+ const units = normalizeUnits({
381
+ succeeded: currentTask.units.succeeded,
382
+ failed: currentTask.units.failed + amount,
383
+ total: currentTask.units.total
384
+ });
367
385
  const nextTasks = new Map(current.tasks);
368
386
  nextTasks.set(taskId, TaskSnapshot({
369
- id: currentTask.id,
370
- parentId: currentTask.parentId,
371
- description: currentTask.description,
372
- status: currentTask.status,
373
- countDisplay: currentTask.countDisplay,
374
- transient: currentTask.transient,
375
- units: normalizeUnits({
376
- succeeded: currentTask.units.succeeded,
377
- failed: currentTask.units.failed + amount,
378
- total: currentTask.units.total
379
- }),
380
- startedAt: currentTask.startedAt,
381
- completedAt: currentTask.completedAt,
382
- metadata: currentTask.metadata
387
+ ...currentTask,
388
+ units,
389
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
383
390
  }));
384
391
  return {
385
392
  state: {
@@ -422,25 +429,21 @@ const makeProgressRenderStore = () => {
422
429
  events: [new TaskCompletedEvent({ taskId }), ...removedTaskIds.map((removedTaskId) => new TaskRemovedEvent({ taskId: removedTaskId }))]
423
430
  };
424
431
  }
432
+ const units = currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
433
+ succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
434
+ failed: currentTask.units.failed,
435
+ total: currentTask.units.total
436
+ }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
437
+ succeeded: currentTask.units.succeeded,
438
+ failed: currentTask.units.failed,
439
+ total: currentTask.units.processed
440
+ }) : currentTask.units;
425
441
  nextTasks.set(taskId, TaskSnapshot({
426
- id: currentTask.id,
427
- parentId: currentTask.parentId,
428
- description: currentTask.description,
442
+ ...currentTask,
429
443
  status: "done",
430
- countDisplay: currentTask.countDisplay,
431
- transient: currentTask.transient,
432
- units: currentTask.units.total !== void 0 ? currentTask.units.processed < currentTask.units.total ? normalizeUnits({
433
- succeeded: currentTask.units.succeeded + (currentTask.units.total - currentTask.units.processed),
434
- failed: currentTask.units.failed,
435
- total: currentTask.units.total
436
- }) : currentTask.units : currentTask.units.processed > 0 ? normalizeUnits({
437
- succeeded: currentTask.units.succeeded,
438
- failed: currentTask.units.failed,
439
- total: currentTask.units.processed
440
- }) : currentTask.units,
441
- startedAt: currentTask.startedAt,
444
+ units,
442
445
  completedAt: now,
443
- metadata: currentTask.metadata
446
+ progressSamples: appendProgressSample(currentTask.progressSamples, now, units.processed)
444
447
  }));
445
448
  return {
446
449
  state: {
@@ -480,16 +483,9 @@ const makeProgressRenderStore = () => {
480
483
  };
481
484
  }
482
485
  nextTasks.set(taskId, TaskSnapshot({
483
- id: currentTask.id,
484
- parentId: currentTask.parentId,
485
- description: currentTask.description,
486
+ ...currentTask,
486
487
  status: "failed",
487
- countDisplay: currentTask.countDisplay,
488
- transient: currentTask.transient,
489
- units: currentTask.units,
490
- startedAt: currentTask.startedAt,
491
- completedAt: now,
492
- metadata: currentTask.metadata
488
+ completedAt: now
493
489
  }));
494
490
  return {
495
491
  state: {
@@ -512,15 +508,7 @@ const makeProgressRenderStore = () => {
512
508
  };
513
509
  const nextTasks = new Map(current.tasks);
514
510
  nextTasks.set(taskId, TaskSnapshot({
515
- id: currentTask.id,
516
- parentId: currentTask.parentId,
517
- description: currentTask.description,
518
- status: currentTask.status,
519
- countDisplay: currentTask.countDisplay,
520
- transient: currentTask.transient,
521
- units: currentTask.units,
522
- startedAt: currentTask.startedAt,
523
- completedAt: currentTask.completedAt,
511
+ ...currentTask,
524
512
  metadata
525
513
  }));
526
514
  return {
@@ -635,17 +623,58 @@ const formatDurationSeconds = (seconds) => {
635
623
  const mins = Math.floor(value % 3600 / 60);
636
624
  return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
637
625
  };
626
+ const formatClockDurationSeconds = (seconds) => {
627
+ const value = Math.max(0, Math.floor(seconds));
628
+ const hours = Math.floor(value / 3600);
629
+ const mins = Math.floor(value % 3600 / 60);
630
+ const secs = value % 60;
631
+ const clock = `${`${mins}`.padStart(2, "0")}:${`${secs}`.padStart(2, "0")}`;
632
+ return hours > 0 ? `${`${hours}`.padStart(2, "0")}:${clock}` : clock;
633
+ };
634
+ /**
635
+ * Estimates remaining time from the task's retained progress sample deque.
636
+ *
637
+ * Returns undefined until there are at least two samples with positive processed and time deltas.
638
+ */
639
+ const getSmoothedEtaMillis = (task) => {
640
+ const { processed, total } = task.units;
641
+ const remaining = total - processed;
642
+ if (processed <= 0 || remaining <= 0) return;
643
+ const samples = task.progressSamples;
644
+ const lastSample = samples.at(-1);
645
+ if (lastSample === void 0) return;
646
+ const firstSample = samples[0];
647
+ if (firstSample === void 0 || firstSample === lastSample) return;
648
+ const deltaProcessed = lastSample.processed - firstSample.processed;
649
+ const deltaMillis = lastSample.timestamp - firstSample.timestamp;
650
+ if (deltaProcessed <= 0 || deltaMillis <= 0) return;
651
+ return Math.max(0, Math.floor(remaining * deltaMillis / deltaProcessed));
652
+ };
638
653
  const formatElapsed = (task, now) => {
639
654
  return formatDurationSeconds(Math.max(0, (task.completedAt ?? now) - task.startedAt) / 1e3);
640
655
  };
656
+ const formatElapsedClock = (task, now) => {
657
+ return formatClockDurationSeconds(Math.max(0, (task.completedAt ?? now) - task.startedAt) / 1e3);
658
+ };
641
659
  const formatEta = (task, now) => {
642
660
  if (task.status !== "running" || !isDeterminate$1(task)) return "";
643
661
  const { processed, total } = task.units;
644
662
  const remaining = total - processed;
645
663
  if (processed <= 0 || remaining <= 0) return "";
646
- const elapsedMillis = Math.max(1, now - task.startedAt);
647
- return formatDurationSeconds(Math.max(0, Math.floor(elapsedMillis / processed * remaining)) / 1e3);
664
+ const etaMillis = getSmoothedEtaMillis(task);
665
+ if (etaMillis === void 0) return "";
666
+ return formatClockDurationSeconds(etaMillis / 1e3);
648
667
  };
668
+ const formatEtaClock = (task, now) => {
669
+ if (task.status !== "running" || !isDeterminate$1(task)) return;
670
+ const { processed, total } = task.units;
671
+ const remaining = total - processed;
672
+ if (processed <= 0 || remaining <= 0) return;
673
+ const etaMillis = getSmoothedEtaMillis(task);
674
+ if (etaMillis === void 0) return;
675
+ return formatClockDurationSeconds(etaMillis / 1e3);
676
+ };
677
+ const formatElapsedEta = (task, now) => `${formatElapsedClock(task, now)}<${formatEtaClock(task, now) ?? "00:00"}`;
649
678
  const formatDeterminateAmountParts = (task) => {
650
679
  if (!isDeterminate$1(task)) return;
651
680
  const totalText = `${task.units.total}`;
@@ -826,7 +855,10 @@ const getTaskIndicator = (task, tick, spinnerType = DEFAULT_SPINNER_TYPE) => {
826
855
  color: "green"
827
856
  };
828
857
  };
829
- const prepareDescription = (rows) => ({ minTreeWidth: rows.reduce((max, row) => Math.max(max, row.derived.treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH), MIN_TREE_DESCRIPTION_TEXT_WIDTH + 2) });
858
+ const prepareDescription = (rows) => ({
859
+ minTreeWidth: rows.reduce((max, row) => Math.max(max, row.derived.treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH), MIN_TREE_DESCRIPTION_TEXT_WIDTH + 2),
860
+ preferredWidth: rows.reduce((max, row) => Math.max(max, row.derived.treePrefixWidth + 2 + row.derived.descriptionWidth), 2)
861
+ });
830
862
  const TaskIndicatorGlyph = ({ task, tick, spinnerType = DEFAULT_SPINNER_TYPE }) => {
831
863
  const indicator = getTaskIndicator(task, tick, spinnerType);
832
864
  return /* @__PURE__ */ jsx(Text, {
@@ -861,6 +893,16 @@ const DescriptionCell = ({ cell, width, minTreeWidth, spinnerTick }) => {
861
893
  });
862
894
  };
863
895
 
896
+ //#endregion
897
+ //#region src/renderer/columns/elapsed-eta-column.tsx
898
+ const ElapsedEtaCell = ({ task, now }) => {
899
+ return /* @__PURE__ */ jsx(Text, {
900
+ wrap: "truncate-end",
901
+ color: "gray",
902
+ children: formatElapsedEta(task, now)
903
+ });
904
+ };
905
+
864
906
  //#endregion
865
907
  //#region src/renderer/columns/elapsed-column.tsx
866
908
  const ElapsedCell = ({ task, now }) => {
@@ -891,10 +933,17 @@ var columns_exports = /* @__PURE__ */ __exportAll({
891
933
  defaults: () => defaults,
892
934
  description: () => description,
893
935
  elapsed: () => elapsed,
936
+ elapsedEta: () => elapsedEta,
894
937
  eta: () => eta,
895
938
  resolveColumnSizeValue: () => resolveColumnSizeValue,
896
939
  spacer: () => spacer
897
940
  });
941
+ const DEFAULT_BAR_SIZE = 30;
942
+ const resolveBarSize = (size) => {
943
+ if (size === "fullwidth") return size;
944
+ if (typeof size !== "number" || !Number.isFinite(size)) return DEFAULT_BAR_SIZE;
945
+ return Math.max(1, Math.floor(size));
946
+ };
898
947
  const spacer = ({ flexGrow, flexShrink, flexBasis, minWidth } = {}) => ({
899
948
  render: () => null,
900
949
  flexGrow,
@@ -904,8 +953,8 @@ const spacer = ({ flexGrow, flexShrink, flexBasis, minWidth } = {}) => ({
904
953
  });
905
954
  const description = () => ({
906
955
  prepare: prepareDescription,
907
- flexGrow: 1,
908
956
  flexShrink: 1,
957
+ flexBasis: (prepared) => prepared.preferredWidth,
909
958
  minWidth: 1,
910
959
  render: (cell, ctx) => /* @__PURE__ */ jsx(DescriptionCell, {
911
960
  cell,
@@ -914,16 +963,20 @@ const description = () => ({
914
963
  spinnerTick: ctx.spinnerTick
915
964
  })
916
965
  });
917
- const bar = () => ({
918
- prepare: prepareBar,
919
- flexShrink: (prepared) => prepared.hasDeterminateRows ? 1 : 0,
920
- flexBasis: (prepared) => prepared.hasDeterminateRows ? 30 : 0,
921
- minWidth: (prepared) => prepared.hasDeterminateRows ? 4 : 0,
922
- render: ({ task }, ctx) => /* @__PURE__ */ jsx(BarCell, {
923
- task,
924
- width: ctx.width
925
- })
926
- });
966
+ const bar = ({ size } = {}) => {
967
+ const resolvedSize = resolveBarSize(size);
968
+ return {
969
+ prepare: prepareBar,
970
+ flexGrow: (prepared) => prepared.hasDeterminateRows && resolvedSize === "fullwidth" ? 1 : 0,
971
+ flexShrink: (prepared) => prepared.hasDeterminateRows ? 1 : 0,
972
+ flexBasis: (prepared) => prepared.hasDeterminateRows ? resolvedSize === "fullwidth" ? DEFAULT_BAR_SIZE : resolvedSize : 0,
973
+ minWidth: (prepared) => prepared.hasDeterminateRows ? resolvedSize === "fullwidth" ? 4 : resolvedSize : 0,
974
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(BarCell, {
975
+ task,
976
+ width: ctx.width
977
+ })
978
+ };
979
+ };
927
980
  const amount = () => ({
928
981
  prepare: measureAmountLayout,
929
982
  align: "right",
@@ -940,9 +993,19 @@ const elapsed = () => ({
940
993
  now: ctx.now
941
994
  })
942
995
  });
996
+ const elapsedEta = () => ({
997
+ align: "right",
998
+ flexShrink: 0,
999
+ minWidth: 11,
1000
+ render: ({ task }, ctx) => /* @__PURE__ */ jsx(ElapsedEtaCell, {
1001
+ task,
1002
+ now: ctx.now
1003
+ })
1004
+ });
943
1005
  const eta = () => ({
944
1006
  align: "right",
945
1007
  flexShrink: 0,
1008
+ minWidth: 8,
946
1009
  render: ({ task }, ctx) => /* @__PURE__ */ jsx(EtaCell, {
947
1010
  task,
948
1011
  now: ctx.now
@@ -952,8 +1015,7 @@ const defaults = () => [
952
1015
  description(),
953
1016
  bar(),
954
1017
  amount(),
955
- elapsed(),
956
- eta()
1018
+ elapsedEta()
957
1019
  ];
958
1020
  const resolveColumnSizeValue = (value, prepared) => {
959
1021
  if (typeof value === "function") return value(prepared);
@@ -1431,4 +1493,4 @@ const forEach = dual(3, (iterable, f, options) => provideProgress(Effect.gen(fun
1431
1493
  })));
1432
1494
 
1433
1495
  //#endregion
1434
- export { columns_exports as Columns, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskRemovedEvent, TaskSnapshot, TaskSnapshotSchema, TaskStatusSchema, TaskUnitsSchema, TaskUpdatedEvent, all, decodeProgressTaskEvent, forEach, task };
1496
+ export { columns_exports as Columns, Progress, ProgressStdio, ProgressTaskEventSchema, Task, TaskAddedEvent, TaskAdvancedEvent, TaskCompletedEvent, TaskCountDisplaySchema, TaskFailedEvent, TaskId, TaskProgressSampleSchema, TaskRemovedEvent, TaskSnapshot, TaskSnapshotSchema, 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.11.0",
3
+ "version": "0.11.1",
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": {
@@ -44,7 +44,7 @@
44
44
  "dependencies": {
45
45
  "cli-spinners": "^3.4.0",
46
46
  "fast-string-width": "^3.0.2",
47
- "ink": "^6.8.0",
47
+ "ink": "^7.0.1",
48
48
  "react": "^19.2.4"
49
49
  },
50
50
  "devDependencies": {
@@ -64,8 +64,5 @@
64
64
  },
65
65
  "engines": {
66
66
  "node": ">=20.12.0"
67
- },
68
- "patchedDependencies": {
69
- "ink@6.8.0": "patches/ink@6.8.0.patch"
70
67
  }
71
68
  }