effective-progress 0.8.0 → 0.8.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.
Files changed (3) hide show
  1. package/README.md +4 -3
  2. package/dist/index.mjs +645 -732
  3. package/package.json +2 -1
package/dist/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
- import { Brand, Cause, Clock, Context, Effect, Exit, FiberRef, Layer, Option, Schema } from "effect";
1
+ import { Brand, Cause, Clock, Context, Data, Effect, Exit, FiberRef, Hash, 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";
4
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
5
  import stringWidth from "fast-string-width";
6
6
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
7
7
 
@@ -447,6 +447,33 @@ const makeProgressRenderStore = () => {
447
447
  };
448
448
  };
449
449
 
450
+ //#endregion
451
+ //#region src/ink-renderer/columns/frame.ts
452
+ const createRenderFrame = (rows, now, tick, stickyWidths) => {
453
+ const tasks = /* @__PURE__ */ new Map();
454
+ const trees = /* @__PURE__ */ new Map();
455
+ for (const row of rows) {
456
+ tasks.set(row.task.id, row.task);
457
+ trees.set(row.task.id, row.tree);
458
+ }
459
+ return {
460
+ taskIds: rows.map((row) => row.task.id),
461
+ now,
462
+ tick,
463
+ stickyWidths,
464
+ getTask: (taskId) => {
465
+ const task = tasks.get(taskId);
466
+ if (task === void 0) throw new Error(`Unknown task id: ${taskId}`);
467
+ return task;
468
+ },
469
+ getTree: (taskId) => {
470
+ const tree = trees.get(taskId);
471
+ if (tree === void 0) throw new Error(`Unknown task tree: ${taskId}`);
472
+ return tree;
473
+ }
474
+ };
475
+ };
476
+
450
477
  //#endregion
451
478
  //#region src/ink-renderer/format.ts
452
479
  const SPINNER_FRAMES = [
@@ -529,6 +556,16 @@ const formatDeterminateAmountParts = (task) => {
529
556
  total: totalText
530
557
  };
531
558
  };
559
+ const getDeterminateProcessedColor = (task) => {
560
+ if (!isDeterminate$1(task)) return "whiteBright";
561
+ const { succeeded, failed, processed, total } = task.units;
562
+ if (task.status === "failed" && processed < total) return "red";
563
+ if (processed >= total && failed === 0) return "green";
564
+ if (processed >= total && failed > 0 && succeeded === 0) return "red";
565
+ if (succeeded > 0 && failed > 0) return "yellow";
566
+ if (failed > 0 && succeeded === 0) return "red";
567
+ return "whiteBright";
568
+ };
532
569
  const formatAmount = (task, _tick) => {
533
570
  if (isDeterminate$1(task)) {
534
571
  const parts = formatDeterminateAmountParts(task);
@@ -545,12 +582,49 @@ const formatAmount = (task, _tick) => {
545
582
  };
546
583
 
547
584
  //#endregion
548
- //#region src/ink-renderer/columns/determinate.ts
549
- const isDeterminate = (task) => task.units.total !== void 0;
550
- const hasDeterminateRows = (rows) => rows.some((row) => isDeterminate(row.task));
585
+ //#region src/ink-renderer/columns/node.ts
586
+ const createColumnDefinition = (config, create) => {
587
+ return {
588
+ id: Hash.hash(Data.struct(config)).toString(36),
589
+ build: (frame) => create(frame, config)
590
+ };
591
+ };
551
592
 
552
593
  //#endregion
553
- //#region src/ink-renderer/columns/spec.ts
594
+ //#region src/ink-renderer/columns/sticky-width.ts
595
+ const applyStickyWidth = ({ key, measure, stickyWidths }) => {
596
+ const preferred = Math.max(measure.preferred, stickyWidths.get(key) ?? 0);
597
+ const max = measure.max === void 0 ? void 0 : Math.max(measure.max, preferred);
598
+ return {
599
+ ...measure,
600
+ preferred,
601
+ max
602
+ };
603
+ };
604
+ const commitStickyWidth = ({ key, measure, stickyWidths }) => {
605
+ stickyWidths.set(key, measure.preferred);
606
+ };
607
+ const createStickyColumn = ({ frame, measure: baseMeasure, render, stickyKey }) => {
608
+ const measure = stickyKey === void 0 ? baseMeasure : applyStickyWidth({
609
+ key: stickyKey,
610
+ measure: baseMeasure,
611
+ stickyWidths: frame.stickyWidths
612
+ });
613
+ return {
614
+ measure,
615
+ commitStickyWidth: stickyKey === void 0 ? void 0 : () => {
616
+ commitStickyWidth({
617
+ key: stickyKey,
618
+ measure,
619
+ stickyWidths: frame.stickyWidths
620
+ });
621
+ },
622
+ render
623
+ };
624
+ };
625
+
626
+ //#endregion
627
+ //#region src/ink-renderer/columns/text-width.ts
554
628
  const WIDTH_CACHE_LIMIT = 4096;
555
629
  const widthCache = /* @__PURE__ */ new Map();
556
630
  const textWidth = (text) => {
@@ -561,140 +635,187 @@ const textWidth = (text) => {
561
635
  widthCache.set(text, width);
562
636
  return width;
563
637
  };
564
- const resolveColumnSpec = (spec, resistance) => ({
565
- id: spec.id,
566
- grow: spec.grow,
567
- canHide: spec.canHide,
568
- variants: spec.variants.map((variant) => ({
569
- id: variant.id,
570
- minWidth: variant.minWidth,
571
- idealWidth: variant.idealWidth,
572
- maxWidth: variant.maxWidth,
573
- shrinkResistance: resistance.shrink(spec.id, variant.id),
574
- demoteResistance: resistance.demote(spec.id, variant.id),
575
- hideResistance: resistance.hide(spec.id, variant.id),
576
- renderCell: variant.renderCell
577
- }))
578
- });
579
- const resolveColumnSpecs = (specs, resistance) => specs.flatMap((spec) => spec.variants.length > 0 ? [resolveColumnSpec(spec, resistance)] : []);
580
638
 
581
639
  //#endregion
582
- //#region src/ink-renderer/columns/amount-column.tsx
583
- const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
584
- const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
585
- const blank = (width) => " ".repeat(Math.max(0, width));
586
- const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
587
- const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
588
- const SucceededCountColumn = ({ task, width }) => {
589
- if (width <= 0) return null;
590
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
591
- if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
592
- return /* @__PURE__ */ jsx(Text, {
593
- color: "green",
594
- children: padLeft(`${task.units.succeeded}`, width)
640
+ //#region src/ink-renderer/columns/description-column.tsx
641
+ const MIN_PLAIN_DESCRIPTION_WIDTH = 8;
642
+ const MIN_COMPACT_DESCRIPTION_WIDTH = 3;
643
+ const MIN_SPINNER_WIDTH = 1;
644
+ const MIN_TREE_DESCRIPTION_TEXT_WIDTH = 6;
645
+ const DESCRIPTION_TREE_STICKY_KEY = Symbol("description.tree");
646
+ const DESCRIPTION_PLAIN_STICKY_KEY = Symbol("description.plain");
647
+ const DESCRIPTION_COMPACT_STICKY_KEY = Symbol("description.compact");
648
+ const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
649
+ const renderTreePrefix = (tree) => {
650
+ if (tree.depth <= 0) return "";
651
+ return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
652
+ };
653
+ const maxDescriptionWidth = (frame, showTree) => frame.taskIds.reduce((max, taskId) => {
654
+ const treePrefix = showTree ? renderTreePrefix(frame.getTree(taskId)) : "";
655
+ return Math.max(max, textWidth(`${treePrefix}${frame.getTask(taskId).description}`) + 2);
656
+ }, MIN_PLAIN_DESCRIPTION_WIDTH);
657
+ const minTreeDescriptionWidth = (frame) => frame.taskIds.reduce((max, taskId) => {
658
+ const treePrefixWidth = textWidth(renderTreePrefix(frame.getTree(taskId)));
659
+ return Math.max(max, treePrefixWidth + 2 + MIN_TREE_DESCRIPTION_TEXT_WIDTH);
660
+ }, MIN_PLAIN_DESCRIPTION_WIDTH);
661
+ const DescriptionColumn = (frame, config = {}) => {
662
+ const variant = config.variant ?? "plain";
663
+ const showTree = variant === "tree";
664
+ const stickyKey = variant === "tree" ? DESCRIPTION_TREE_STICKY_KEY : variant === "plain" ? DESCRIPTION_PLAIN_STICKY_KEY : variant === "compact" ? DESCRIPTION_COMPACT_STICKY_KEY : void 0;
665
+ const min = variant === "spinner" ? MIN_SPINNER_WIDTH : variant === "compact" ? MIN_COMPACT_DESCRIPTION_WIDTH : variant === "tree" ? minTreeDescriptionWidth(frame) : MIN_PLAIN_DESCRIPTION_WIDTH;
666
+ const preferred = variant === "spinner" ? MIN_SPINNER_WIDTH : Math.max(min, maxDescriptionWidth(frame, showTree));
667
+ return createStickyColumn({
668
+ frame,
669
+ measure: {
670
+ min,
671
+ preferred,
672
+ max: preferred
673
+ },
674
+ stickyKey,
675
+ render: (taskId) => {
676
+ const task = frame.getTask(taskId);
677
+ const tree = frame.getTree(taskId);
678
+ const treePrefix = showTree ? renderTreePrefix(tree) : "";
679
+ const indicator = getTaskIndicator(task, frame.tick);
680
+ if (variant === "spinner") return /* @__PURE__ */ jsx(Text, {
681
+ color: indicator.color,
682
+ children: indicator.symbol
683
+ });
684
+ return /* @__PURE__ */ jsxs(Text, {
685
+ wrap: "truncate-end",
686
+ children: [
687
+ treePrefix,
688
+ /* @__PURE__ */ jsx(Text, {
689
+ color: indicator.color,
690
+ children: indicator.symbol
691
+ }),
692
+ ` ${task.description}`
693
+ ]
694
+ });
695
+ }
595
696
  });
596
697
  };
597
- const FailedCountColumn = ({ task, width }) => {
598
- if (width <= 0) return null;
599
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
600
- if (!shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
601
- return /* @__PURE__ */ jsx(Text, {
602
- color: "red",
603
- children: padLeft(`${task.units.failed}`, width)
698
+ const createDescriptionRootColumn = (variant) => createColumnDefinition({
699
+ _tag: "description",
700
+ variant
701
+ }, (frame, config) => DescriptionColumn(frame, config));
702
+ const DescriptionTreeRootColumn = createDescriptionRootColumn("tree");
703
+ const DescriptionPlainRootColumn = createDescriptionRootColumn("plain");
704
+ const DescriptionCompactRootColumn = createDescriptionRootColumn("compact");
705
+ const DescriptionSpinnerRootColumn = createDescriptionRootColumn("spinner");
706
+
707
+ //#endregion
708
+ //#region src/ink-renderer/columns/elapsed-column.tsx
709
+ const MIN_ELAPSED_WIDTH = Array.from("10s").length;
710
+ const ELAPSED_STICKY_KEY = Symbol("elapsed");
711
+ const maxElapsedWidth = (frame) => frame.taskIds.reduce((max, taskId) => Math.max(max, textWidth(formatElapsed(frame.getTask(taskId), frame.now))), MIN_ELAPSED_WIDTH);
712
+ const ElapsedColumn = (frame) => {
713
+ const elapsedContentWidth = maxElapsedWidth(frame);
714
+ return createStickyColumn({
715
+ frame,
716
+ stickyKey: ELAPSED_STICKY_KEY,
717
+ measure: {
718
+ min: MIN_ELAPSED_WIDTH,
719
+ preferred: elapsedContentWidth,
720
+ max: elapsedContentWidth
721
+ },
722
+ render: (taskId, width) => {
723
+ const formatted = formatElapsed(frame.getTask(taskId), frame.now);
724
+ return /* @__PURE__ */ jsx(Box, {
725
+ width,
726
+ justifyContent: "flex-end",
727
+ children: /* @__PURE__ */ jsx(Text, {
728
+ color: "gray",
729
+ children: textWidth(formatted) <= width ? formatted : formatted.slice(0, width)
730
+ })
731
+ });
732
+ }
604
733
  });
605
734
  };
606
- const ProcessedCountColumn = ({ task, width }) => {
607
- if (width <= 0) return null;
608
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
609
- return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
735
+ const ElapsedRootColumn = createColumnDefinition({ _tag: "elapsed" }, (frame) => ElapsedColumn(frame));
736
+
737
+ //#endregion
738
+ //#region src/ink-renderer/columns/determinate.ts
739
+ const isDeterminate = (task) => task.units.total !== void 0;
740
+
741
+ //#endregion
742
+ //#region src/ink-renderer/columns/eta-column.tsx
743
+ const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
744
+ const ETA_STICKY_KEY = Symbol("eta");
745
+ const etaDurationText = (task, now) => {
746
+ if (task.status !== "running" || !isDeterminate(task)) return;
747
+ const eta = formatEta(task, now);
748
+ return eta.length > 0 ? eta : "--";
610
749
  };
611
- const AmountSeparatorColumn = ({ task, tick }) => {
612
- if (shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: "/" });
613
- const symbol = formatAmount(task, tick);
614
- if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
615
- color: "red",
616
- children: symbol
617
- });
618
- if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
619
- color: "yellow",
620
- children: symbol
621
- });
622
- return /* @__PURE__ */ jsx(Text, { children: symbol });
750
+ const renderEtaText = (task, now, width) => {
751
+ const duration = etaDurationText(task, now);
752
+ if (duration === void 0) return "";
753
+ const prefixed = `ETA: ${duration}`;
754
+ if (width >= textWidth(prefixed)) return prefixed;
755
+ if (width >= textWidth(duration)) return duration;
756
+ return primaryUnit(duration);
623
757
  };
624
- const TotalCountColumn = ({ task, width }) => {
625
- if (width <= 0) return null;
626
- if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
627
- return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
758
+ const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
759
+ const computeEtaMetrics = (frame) => {
760
+ let hasEta = false;
761
+ let prefixedWidth = 0;
762
+ let durationWidth = 0;
763
+ let primaryUnitWidth = 0;
764
+ for (const taskId of frame.taskIds) {
765
+ const duration = etaDurationText(frame.getTask(taskId), frame.now);
766
+ if (duration === void 0) continue;
767
+ hasEta = true;
768
+ const prefixed = `ETA: ${duration}`;
769
+ prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
770
+ durationWidth = Math.max(durationWidth, textWidth(duration));
771
+ primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
772
+ }
773
+ return {
774
+ hasEta,
775
+ prefixedWidth,
776
+ durationWidth: Math.max(2, durationWidth),
777
+ primaryUnitWidth: Math.max(2, primaryUnitWidth)
778
+ };
628
779
  };
629
- const AmountColumn = ({ task, tick, layout }) => {
630
- if (layout.kind === "text") {
631
- const text = formatAmount(task, tick);
632
- if (task.status === "failed") return /* @__PURE__ */ jsx(Text, {
633
- wrap: "truncate-end",
634
- color: "red",
635
- children: text
636
- });
637
- if (task.status === "running") return /* @__PURE__ */ jsx(Text, {
638
- wrap: "truncate-end",
639
- color: "yellow",
640
- children: text
641
- });
642
- return /* @__PURE__ */ jsx(Text, {
780
+ const EtaColumn = (frame) => {
781
+ const metrics = computeEtaMetrics(frame);
782
+ if (!metrics.hasEta) return;
783
+ return createStickyColumn({
784
+ frame,
785
+ stickyKey: ETA_STICKY_KEY,
786
+ measure: {
787
+ min: metrics.primaryUnitWidth,
788
+ preferred: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
789
+ max: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR)
790
+ },
791
+ render: (taskId, width) => /* @__PURE__ */ jsx(Text, {
643
792
  wrap: "truncate-end",
644
- children: text
645
- });
646
- }
647
- if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
648
- flexDirection: "row",
649
- children: [
650
- /* @__PURE__ */ jsx(ProcessedCountColumn, {
651
- task,
652
- width: layout.processedWidth
653
- }),
654
- /* @__PURE__ */ jsx(AmountSeparatorColumn, {
655
- task,
656
- tick
657
- }),
658
- /* @__PURE__ */ jsx(TotalCountColumn, {
659
- task,
660
- width: layout.totalWidth
661
- })
662
- ]
663
- });
664
- return /* @__PURE__ */ jsxs(Box, {
665
- flexDirection: "row",
666
- children: [
667
- layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SucceededCountColumn, {
668
- task,
669
- width: layout.succeededWidth
670
- }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
671
- layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(FailedCountColumn, {
672
- task,
673
- width: layout.failedWidth
674
- }), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
675
- /* @__PURE__ */ jsx(ProcessedCountColumn, {
676
- task,
677
- width: layout.processedWidth
678
- }),
679
- /* @__PURE__ */ jsx(AmountSeparatorColumn, {
680
- task,
681
- tick
682
- }),
683
- /* @__PURE__ */ jsx(TotalCountColumn, {
684
- task,
685
- width: layout.totalWidth
686
- })
687
- ]
793
+ color: "gray",
794
+ children: renderEtaText(frame.getTask(taskId), frame.now, width)
795
+ })
688
796
  });
689
797
  };
690
- const computeAmountMetrics = (rows, tick) => {
798
+ const EtaRootColumn = createColumnDefinition({ _tag: "eta" }, (frame) => EtaColumn(frame));
799
+
800
+ //#endregion
801
+ //#region src/ink-renderer/columns/progress/shared.ts
802
+ const DEFAULT_BAR_WIDTH = 30;
803
+ const PERCENT_FALLBACK_WIDTH = 10;
804
+ const padLeft = (value, width) => value.padStart(Math.max(0, width), " ");
805
+ const padRight = (value, width) => value.padEnd(Math.max(0, width), " ");
806
+ const blank = (width) => " ".repeat(Math.max(0, width));
807
+ const shouldShowCountAmount = (task) => task.units.total !== void 0 || task.units.processed > 0;
808
+ const shouldShowDetailedCounts = (task) => shouldShowCountAmount(task) && task.countDisplay === "detailed";
809
+ const computeProgressMetrics = (frame) => {
691
810
  let hasStructuredCounts = false;
692
811
  let hasDetailed = false;
812
+ let hasDeterminate = false;
693
813
  let countDigits = 0;
694
814
  let totalWidth = 0;
695
815
  let simpleTextWidth = 0;
696
- for (const row of rows) {
697
- const { task } = row;
816
+ for (const taskId of frame.taskIds) {
817
+ const task = frame.getTask(taskId);
818
+ hasDeterminate ||= isDeterminate(task);
698
819
  if (shouldShowCountAmount(task)) {
699
820
  hasStructuredCounts = true;
700
821
  countDigits = Math.max(countDigits, textWidth(`${task.units.succeeded}`), textWidth(`${task.units.failed}`), textWidth(`${task.units.processed}`));
@@ -702,85 +823,124 @@ const computeAmountMetrics = (rows, tick) => {
702
823
  if (task.countDisplay === "detailed") hasDetailed = true;
703
824
  continue;
704
825
  }
705
- simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, tick)));
826
+ simpleTextWidth = Math.max(simpleTextWidth, textWidth(formatAmount(task, frame.tick)));
706
827
  }
707
828
  return {
708
829
  hasStructuredCounts,
709
830
  hasDetailed,
831
+ hasDeterminate,
710
832
  countDigits: Math.max(1, countDigits),
711
833
  totalWidth: Math.max(1, totalWidth),
712
834
  simpleTextWidth
713
835
  };
714
836
  };
715
- const detailedAmountLayout = (metrics) => ({
716
- kind: "detailed",
717
- succeededWidth: metrics.hasDetailed ? metrics.countDigits : 0,
718
- failedWidth: metrics.hasDetailed ? metrics.countDigits : 0,
719
- processedWidth: metrics.countDigits,
720
- totalWidth: metrics.totalWidth
721
- });
722
- const processedAmountLayout = (metrics) => ({
723
- kind: "processed",
724
- processedWidth: metrics.countDigits,
725
- totalWidth: metrics.totalWidth
726
- });
727
- const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
728
837
  const processedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth;
729
- const createAmountColumnSpec = (context) => {
730
- const metrics = computeAmountMetrics(context.rows, context.tick);
731
- if (!metrics.hasStructuredCounts && metrics.simpleTextWidth <= 0) return;
732
- const detailedLayout = detailedAmountLayout(metrics);
733
- const processedLayout = processedAmountLayout(metrics);
838
+ const detailedAmountWidth = (metrics) => metrics.countDigits + 1 + metrics.totalWidth + (metrics.hasDetailed ? metrics.countDigits + 1 + metrics.countDigits + 1 : 0);
839
+ const percentText = (task) => {
840
+ if (!isDeterminate(task)) return formatAmount(task, 0);
841
+ if (task.units.total === 0) return "100%";
842
+ const displayTotal = Math.max(task.units.total, task.units.processed);
843
+ return `${Math.max(0, Math.min(100, Math.round(task.units.processed / displayTotal * 100)))}%`;
844
+ };
845
+
846
+ //#endregion
847
+ //#region src/ink-renderer/columns/progress/amount-column.tsx
848
+ const succeededCount = (task, width) => {
849
+ if (width <= 0 || !shouldShowCountAmount(task) || !shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
850
+ return /* @__PURE__ */ jsx(Text, {
851
+ color: "green",
852
+ children: padLeft(`${task.units.succeeded}`, width)
853
+ });
854
+ };
855
+ const failedCount = (task, width) => {
856
+ if (width <= 0 || !shouldShowCountAmount(task) || !shouldShowDetailedCounts(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
857
+ return /* @__PURE__ */ jsx(Text, {
858
+ color: "red",
859
+ children: padLeft(`${task.units.failed}`, width)
860
+ });
861
+ };
862
+ const processedCount = (task, width) => {
863
+ if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
864
+ return /* @__PURE__ */ jsx(Text, { children: padLeft(`${task.units.processed}`, width) });
865
+ };
866
+ const totalCount = (task, width) => {
867
+ if (width <= 0 || !shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
868
+ return /* @__PURE__ */ jsx(Text, { children: padRight(isDeterminate(task) ? `${task.units.total}` : "?", width) });
869
+ };
870
+ const structuredAmount = (task, tick, width, layout) => {
871
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
872
+ wrap: "truncate-end",
873
+ children: formatAmount(task, tick)
874
+ });
875
+ if (layout.kind === "processed") return /* @__PURE__ */ jsxs(Box, {
876
+ flexDirection: "row",
877
+ width,
878
+ children: [
879
+ processedCount(task, layout.processedWidth),
880
+ /* @__PURE__ */ jsx(Text, { children: "/" }),
881
+ totalCount(task, layout.totalWidth)
882
+ ]
883
+ });
884
+ return /* @__PURE__ */ jsxs(Box, {
885
+ flexDirection: "row",
886
+ width,
887
+ children: [
888
+ layout.succeededWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [succeededCount(task, layout.succeededWidth), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
889
+ layout.failedWidth > 0 ? /* @__PURE__ */ jsxs(Fragment, { children: [failedCount(task, layout.failedWidth), /* @__PURE__ */ jsx(Text, { children: ` ` })] }) : null,
890
+ processedCount(task, layout.processedWidth),
891
+ /* @__PURE__ */ jsx(Text, { children: "/" }),
892
+ totalCount(task, layout.totalWidth)
893
+ ]
894
+ });
895
+ };
896
+ const AmountColumn = (frame, config) => {
897
+ const metrics = config.metrics ?? computeProgressMetrics(frame);
734
898
  const detailedWidth = detailedAmountWidth(metrics);
735
899
  const processedWidth = processedAmountWidth(metrics);
736
- return {
737
- id: "amount",
738
- grow: 0,
739
- canHide: true,
740
- variants: metrics.hasStructuredCounts && metrics.hasDetailed ? [{
741
- id: "detailed",
742
- minWidth: detailedWidth,
743
- idealWidth: detailedWidth,
744
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
745
- task: row.task,
746
- tick: context.tick,
747
- layout: detailedLayout
748
- })
749
- }, {
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
- }] : metrics.hasStructuredCounts ? [{
759
- id: "processed",
760
- minWidth: processedWidth,
761
- idealWidth: processedWidth,
762
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
763
- task: row.task,
764
- tick: context.tick,
765
- layout: processedLayout
766
- })
767
- }] : [{
768
- id: "text",
769
- minWidth: 0,
770
- idealWidth: metrics.simpleTextWidth,
771
- renderCell: (row) => /* @__PURE__ */ jsx(AmountColumn, {
772
- task: row.task,
773
- tick: context.tick,
774
- layout: { kind: "text" }
775
- })
776
- }]
900
+ const preferredWidth = metrics.hasStructuredCounts ? metrics.hasDetailed ? detailedWidth : processedWidth : Math.max(1, metrics.simpleTextWidth);
901
+ const minWidth = metrics.hasStructuredCounts ? processedWidth : Math.max(1, metrics.simpleTextWidth);
902
+ const summary = {
903
+ hasDetailed: metrics.hasDetailed,
904
+ countDigits: metrics.countDigits,
905
+ totalWidth: metrics.totalWidth,
906
+ detailedWidth,
907
+ processedWidth,
908
+ preferredWidth,
909
+ minWidth,
910
+ simpleTextWidth: Math.max(1, metrics.simpleTextWidth)
777
911
  };
912
+ return createStickyColumn({
913
+ frame,
914
+ measure: {
915
+ min: summary.minWidth,
916
+ preferred: summary.preferredWidth,
917
+ max: summary.preferredWidth
918
+ },
919
+ stickyKey: config.stickyWidth === true ? config.key : void 0,
920
+ render: (taskId, width) => {
921
+ const task = frame.getTask(taskId);
922
+ if (!shouldShowCountAmount(task)) return /* @__PURE__ */ jsx(Text, {
923
+ wrap: "truncate-end",
924
+ children: formatAmount(task, frame.tick)
925
+ });
926
+ if (summary.hasDetailed && width >= summary.detailedWidth) return structuredAmount(task, frame.tick, width, {
927
+ kind: "detailed",
928
+ succeededWidth: summary.countDigits,
929
+ failedWidth: summary.countDigits,
930
+ processedWidth: summary.countDigits,
931
+ totalWidth: summary.totalWidth
932
+ });
933
+ return structuredAmount(task, frame.tick, width, {
934
+ kind: "processed",
935
+ processedWidth: summary.countDigits,
936
+ totalWidth: summary.totalWidth
937
+ });
938
+ }
939
+ });
778
940
  };
779
941
 
780
942
  //#endregion
781
- //#region src/ink-renderer/columns/bar-column.tsx
782
- const DEFAULT_BAR_WIDTH = 30;
783
- const MIN_BAR_WIDTH = 8;
943
+ //#region src/ink-renderer/columns/progress/bar-column.tsx
784
944
  const segmentLengths = (width, total, succeeded, failed) => {
785
945
  if (total === 0) return {
786
946
  succeeded: width,
@@ -798,576 +958,335 @@ const segmentLengths = (width, total, succeeded, failed) => {
798
958
  remaining: Math.max(0, width - succeededLength - failedLength)
799
959
  };
800
960
  };
801
- const BarColumn = ({ task, width }) => {
802
- if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, {});
803
- const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
804
- return /* @__PURE__ */ jsxs(Text, {
805
- wrap: "truncate-end",
806
- children: [
807
- /* @__PURE__ */ jsx(Text, {
808
- color: "green",
809
- children: "━".repeat(lengths.succeeded)
810
- }),
811
- /* @__PURE__ */ jsx(Text, {
812
- color: "red",
813
- children: "━".repeat(lengths.failed)
814
- }),
815
- /* @__PURE__ */ jsx(Text, {
816
- color: "gray",
817
- children: "─".repeat(lengths.remaining)
818
- })
819
- ]
961
+ const BarColumn = (frame, config) => {
962
+ return createStickyColumn({
963
+ frame,
964
+ measure: {
965
+ min: 4,
966
+ preferred: DEFAULT_BAR_WIDTH,
967
+ max: config.fullWidth ? void 0 : DEFAULT_BAR_WIDTH
968
+ },
969
+ stickyKey: config.stickyWidth === true ? config.key : void 0,
970
+ render: (taskId, width) => {
971
+ const task = frame.getTask(taskId);
972
+ if (!isDeterminate(task)) return /* @__PURE__ */ jsx(Text, { children: blank(width) });
973
+ const lengths = segmentLengths(Math.max(1, Math.floor(width)), task.units.total, task.units.succeeded, task.units.failed);
974
+ return /* @__PURE__ */ jsxs(Text, {
975
+ wrap: "truncate-end",
976
+ children: [
977
+ /* @__PURE__ */ jsx(Text, {
978
+ color: "green",
979
+ children: "━".repeat(lengths.succeeded)
980
+ }),
981
+ /* @__PURE__ */ jsx(Text, {
982
+ color: "red",
983
+ children: "━".repeat(lengths.failed)
984
+ }),
985
+ /* @__PURE__ */ jsx(Text, {
986
+ color: "gray",
987
+ children: "─".repeat(lengths.remaining)
988
+ })
989
+ ]
990
+ });
991
+ }
820
992
  });
821
993
  };
822
- const createBarColumnSpec = (context, isTTY) => {
823
- if (!hasDeterminateRows(context.rows)) return;
824
- return {
825
- id: "bar",
826
- grow: 0,
827
- canHide: true,
828
- variants: [{
829
- id: "full",
830
- minWidth: MIN_BAR_WIDTH,
831
- idealWidth: DEFAULT_BAR_WIDTH,
832
- maxWidth: DEFAULT_BAR_WIDTH,
833
- renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
834
- task: row.task,
835
- tree: row.tree,
836
- now: context.now,
837
- tick: context.tick,
838
- isTTY,
839
- width: Math.max(1, Math.min(width, DEFAULT_BAR_WIDTH))
840
- })
841
- }, {
842
- id: "compact",
843
- minWidth: 1,
844
- idealWidth: MIN_BAR_WIDTH,
845
- maxWidth: MIN_BAR_WIDTH,
846
- renderCell: (row, width) => /* @__PURE__ */ jsx(BarColumn, {
847
- task: row.task,
848
- tree: row.tree,
849
- now: context.now,
850
- tick: context.tick,
851
- isTTY,
852
- width: Math.max(1, width)
853
- })
854
- }]
855
- };
856
- };
857
994
 
858
995
  //#endregion
859
- //#region src/ink-renderer/view/tree-prefix.ts
860
- const treeAncestorPrefix = (tree) => tree.ancestorHasNextSibling.slice(1).map((hasNext) => hasNext ? "│ " : " ").join("");
861
- const renderTreePrefix = (tree) => {
862
- if (tree.depth <= 0) return "";
863
- return `${treeAncestorPrefix(tree)}${tree.hasNextSibling ? "├─ " : "└─ "}`;
864
- };
996
+ //#region src/ink-renderer/columns/progress/percent-column.tsx
997
+ const PercentColumn = (frame) => ({
998
+ measure: {
999
+ min: 4,
1000
+ preferred: 4,
1001
+ max: 4
1002
+ },
1003
+ render: (taskId, _width) => {
1004
+ const task = frame.getTask(taskId);
1005
+ const determinateColor = isDeterminate(task) ? getDeterminateProcessedColor(task) : void 0;
1006
+ return /* @__PURE__ */ jsx(Text, {
1007
+ wrap: "truncate-end",
1008
+ color: determinateColor === "green" || determinateColor === "yellow" || determinateColor === "red" ? determinateColor : void 0,
1009
+ children: percentText(task)
1010
+ });
1011
+ }
1012
+ });
865
1013
 
866
1014
  //#endregion
867
- //#region src/ink-renderer/columns/description-column.tsx
868
- const DescriptionColumn = ({ task, tree, showTree, tick }) => {
869
- const treePrefix = showTree ? renderTreePrefix(tree) : "";
870
- const indicator = getTaskIndicator(task, tick);
871
- return /* @__PURE__ */ jsxs(Text, {
872
- wrap: "truncate-end",
873
- children: [
874
- treePrefix,
875
- /* @__PURE__ */ jsx(Text, {
876
- color: indicator.color,
877
- children: indicator.symbol
878
- }),
879
- ` ${task.description}`
880
- ]
881
- });
882
- };
883
- const MIN_DESCRIPTION_WIDTH = 8;
884
- const MIN_DESCRIPTION_WITH_TREE_WIDTH = 20;
885
- const DESCRIPTION_PADDING_GROWTH_LIMIT = 20;
886
- const maxDescriptionWidth = (rows, showTree) => rows.reduce((max, row) => {
887
- const treePrefix = showTree ? renderTreePrefix(row.tree) : "";
888
- return Math.max(max, textWidth(`${treePrefix}${row.task.description}`) + 2);
889
- }, MIN_DESCRIPTION_WIDTH);
890
- const createDescriptionColumnSpec = (context, isTTY) => {
891
- const treeIdeal = maxDescriptionWidth(context.rows, true);
892
- const plainIdeal = maxDescriptionWidth(context.rows, false);
893
- const treeVariantIdeal = Math.max(MIN_DESCRIPTION_WITH_TREE_WIDTH, treeIdeal);
894
- const plainVariantIdeal = Math.max(MIN_DESCRIPTION_WIDTH, plainIdeal);
895
- return {
896
- id: "description",
897
- grow: 1,
898
- canHide: false,
899
- variants: [{
900
- id: "tree",
901
- minWidth: MIN_DESCRIPTION_WITH_TREE_WIDTH,
902
- idealWidth: treeVariantIdeal,
903
- maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, treeVariantIdeal),
904
- renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
905
- task: row.task,
906
- tree: row.tree,
907
- now: context.now,
908
- tick: context.tick,
909
- isTTY,
910
- showTree: true
911
- })
912
- }, {
913
- id: "plain",
914
- minWidth: MIN_DESCRIPTION_WIDTH,
915
- idealWidth: plainVariantIdeal,
916
- maxWidth: Math.max(DESCRIPTION_PADDING_GROWTH_LIMIT, plainVariantIdeal),
917
- renderCell: (row) => /* @__PURE__ */ jsx(DescriptionColumn, {
918
- task: row.task,
919
- tree: row.tree,
920
- now: context.now,
921
- tick: context.tick,
922
- isTTY,
923
- showTree: false
924
- })
925
- }]
1015
+ //#region src/ink-renderer/columns/progress-metrics-column.tsx
1016
+ const PROGRESS_BAR_STICKY_KEY = Symbol("progress.bar");
1017
+ const PROGRESS_AMOUNT_STICKY_KEY = Symbol("progress.amount");
1018
+ const createProgressColumnModel = (frame, mode) => {
1019
+ const metrics = computeProgressMetrics(frame);
1020
+ const percent = PercentColumn(frame);
1021
+ if (mode === "percent" && metrics.hasDeterminate) return {
1022
+ metrics,
1023
+ percent
926
1024
  };
927
- };
928
-
929
- //#endregion
930
- //#region src/ink-renderer/columns/elapsed-column.tsx
931
- const ElapsedColumn = ({ task, now }) => /* @__PURE__ */ jsx(Text, {
932
- wrap: "truncate-end",
933
- color: "gray",
934
- children: formatElapsed(task, now)
935
- });
936
- const MIN_ELAPSED_WIDTH = 2;
937
- const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
938
- const maxElapsedWidth = (rows, now) => rows.reduce((max, row) => Math.max(max, textWidth(formatElapsed(row.task, now))), MIN_ELAPSED_WIDTH);
939
- const createElapsedColumnSpec = (context, isTTY) => {
940
- const elapsedContentWidth = maxElapsedWidth(context.rows, context.now);
941
1025
  return {
942
- id: "elapsed",
943
- grow: 0,
944
- canHide: false,
945
- variants: [{
946
- id: "stable",
947
- minWidth: elapsedContentWidth,
948
- idealWidth: hasDeterminateRows(context.rows) ? Math.max(elapsedContentWidth, RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR) : elapsedContentWidth,
949
- renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
950
- task: row.task,
951
- tree: row.tree,
952
- now: context.now,
953
- tick: context.tick,
954
- isTTY
955
- })
956
- }, {
957
- id: "compact",
958
- minWidth: MIN_ELAPSED_WIDTH,
959
- idealWidth: elapsedContentWidth,
960
- renderCell: (row) => /* @__PURE__ */ jsx(ElapsedColumn, {
961
- task: row.task,
962
- tree: row.tree,
963
- now: context.now,
964
- tick: context.tick,
965
- isTTY
966
- })
967
- }]
1026
+ metrics,
1027
+ percent,
1028
+ amount: AmountColumn(frame, {
1029
+ key: PROGRESS_AMOUNT_STICKY_KEY,
1030
+ metrics,
1031
+ stickyWidth: true
1032
+ }),
1033
+ bar: metrics.hasDeterminate ? BarColumn(frame, {
1034
+ key: PROGRESS_BAR_STICKY_KEY,
1035
+ fullWidth: false,
1036
+ stickyWidth: true
1037
+ }) : void 0
968
1038
  };
969
1039
  };
970
-
971
- //#endregion
972
- //#region src/ink-renderer/columns/eta-column.tsx
973
- const primaryUnit = (duration) => duration.split(" ")[0] ?? duration;
974
- const etaDurationText = (task, now) => {
975
- if (task.status !== "running" || !isDeterminate(task)) return;
976
- const eta = formatEta(task, now);
977
- return eta.length > 0 ? eta : "--";
978
- };
979
- const EtaColumn = ({ task, now, mode }) => {
980
- const duration = etaDurationText(task, now);
981
- if (duration === void 0) return /* @__PURE__ */ jsx(Text, {});
982
- return /* @__PURE__ */ jsx(Text, {
983
- wrap: "truncate-end",
984
- color: "gray",
985
- children: mode === "prefixed" ? `ETA: ${duration}` : mode === "primary" ? primaryUnit(duration) : duration
986
- });
987
- };
988
- const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
989
- const computeEtaMetrics = (rows, now) => {
990
- let hasEta = false;
991
- let prefixedWidth = 0;
992
- let durationWidth = 0;
993
- let primaryUnitWidth = 0;
994
- for (const row of rows) {
995
- const duration = etaDurationText(row.task, now);
996
- if (duration === void 0) continue;
997
- hasEta = true;
998
- const prefixed = `ETA: ${duration}`;
999
- prefixedWidth = Math.max(prefixedWidth, textWidth(prefixed));
1000
- durationWidth = Math.max(durationWidth, textWidth(duration));
1001
- primaryUnitWidth = Math.max(primaryUnitWidth, textWidth(primaryUnit(duration)));
1002
- }
1040
+ const layoutForWidth = (width, model) => {
1041
+ if (!model.metrics.hasDeterminate || model.amount === void 0) return { kind: "amount-only" };
1042
+ const bar = model.bar;
1043
+ if (bar === void 0) return { kind: "percent" };
1044
+ if (width < bar.measure.min + 1 + model.amount.measure.min || width < PERCENT_FALLBACK_WIDTH) return { kind: "percent" };
1045
+ const available = Math.max(0, width - 1);
1046
+ const amountWidth = Math.min(model.amount.measure.preferred, Math.max(model.amount.measure.min, available - bar.measure.min));
1003
1047
  return {
1004
- hasEta,
1005
- prefixedWidth,
1006
- durationWidth: Math.max(2, durationWidth),
1007
- primaryUnitWidth: Math.max(2, primaryUnitWidth)
1048
+ kind: "bar-amount",
1049
+ bar,
1050
+ barWidth: Math.max(bar.measure.min, available - amountWidth),
1051
+ amountWidth
1008
1052
  };
1009
1053
  };
1010
- const createEtaColumnSpec = (context, isTTY) => {
1011
- const metrics = computeEtaMetrics(context.rows, context.now);
1012
- if (!metrics.hasEta) return;
1054
+ const ProgressMetricsColumn = (frame, config = {}) => {
1055
+ const mode = config.mode ?? "full";
1056
+ const model = createProgressColumnModel(frame, mode);
1057
+ if (!model.metrics.hasStructuredCounts && !model.metrics.hasDeterminate) return;
1058
+ if (mode === "percent" && model.metrics.hasDeterminate) return model.percent;
1059
+ const amount = model.amount;
1060
+ if (amount === void 0) return;
1061
+ const preferred = model.metrics.hasDeterminate && model.bar !== void 0 ? model.bar.measure.preferred + 1 + amount.measure.preferred : amount.measure.preferred;
1062
+ const fullMin = model.metrics.hasDeterminate && model.bar !== void 0 ? model.bar.measure.min + 1 + amount.measure.min : amount.measure.min;
1063
+ const max = !model.metrics.hasDeterminate || model.bar === void 0 || model.bar.measure.max === void 0 ? amount.measure.max : model.bar.measure.max + 1 + amount.measure.preferred;
1013
1064
  return {
1014
- id: "eta",
1015
- grow: 0,
1016
- canHide: true,
1017
- variants: [
1018
- {
1019
- id: "prefixed",
1020
- minWidth: metrics.prefixedWidth,
1021
- idealWidth: Math.max(metrics.prefixedWidth, RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR),
1022
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1023
- task: row.task,
1024
- tree: row.tree,
1025
- now: context.now,
1026
- tick: context.tick,
1027
- isTTY,
1028
- mode: "prefixed"
1029
- })
1030
- },
1031
- {
1032
- id: "duration",
1033
- minWidth: metrics.durationWidth,
1034
- idealWidth: metrics.durationWidth,
1035
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1036
- task: row.task,
1037
- tree: row.tree,
1038
- now: context.now,
1039
- tick: context.tick,
1040
- isTTY,
1041
- mode: "duration"
1042
- })
1043
- },
1044
- {
1045
- id: "primary",
1046
- minWidth: metrics.primaryUnitWidth,
1047
- idealWidth: metrics.primaryUnitWidth,
1048
- renderCell: (row) => /* @__PURE__ */ jsx(EtaColumn, {
1049
- task: row.task,
1050
- tree: row.tree,
1051
- now: context.now,
1052
- tick: context.tick,
1053
- isTTY,
1054
- mode: "primary"
1055
- })
1056
- }
1057
- ]
1065
+ measure: {
1066
+ min: !model.metrics.hasDeterminate ? amount.measure.min : fullMin,
1067
+ preferred,
1068
+ max
1069
+ },
1070
+ commitStickyWidth: () => {
1071
+ model.bar?.commitStickyWidth?.();
1072
+ amount.commitStickyWidth?.();
1073
+ },
1074
+ render: (taskId, width) => {
1075
+ const layout = layoutForWidth(width, model);
1076
+ if (layout.kind === "percent") return model.percent.render(taskId, width);
1077
+ if (layout.kind === "amount-only") return amount.render(taskId, width);
1078
+ return /* @__PURE__ */ jsxs(Box, {
1079
+ flexDirection: "row",
1080
+ width,
1081
+ children: [
1082
+ /* @__PURE__ */ jsx(Box, {
1083
+ width: layout.barWidth,
1084
+ children: layout.bar.render(taskId, layout.barWidth)
1085
+ }),
1086
+ /* @__PURE__ */ jsx(Box, { marginRight: 1 }),
1087
+ /* @__PURE__ */ jsx(Box, {
1088
+ width: layout.amountWidth,
1089
+ children: amount.render(taskId, layout.amountWidth)
1090
+ })
1091
+ ]
1092
+ });
1093
+ }
1058
1094
  };
1059
1095
  };
1096
+ const createProgressRootColumn = (mode) => createColumnDefinition({
1097
+ _tag: "progress",
1098
+ mode
1099
+ }, (frame, config) => ProgressMetricsColumn(frame, config));
1100
+ const ProgressRootColumn = createProgressRootColumn("full");
1101
+ const ProgressPercentRootColumn = createProgressRootColumn("percent");
1060
1102
 
1061
1103
  //#endregion
1062
- //#region src/ink-renderer/columns/planner.ts
1063
- const clampInt = (value, min, max) => Math.max(min, Math.min(max, Math.floor(value)));
1064
- const activeColumns = (columns) => columns.filter((column) => !column.hidden);
1065
- const visibleColumns = (columns) => columns.filter((column) => !column.hidden && column.width > 0);
1066
- const visibleGapWidth = (columns) => Math.max(0, visibleColumns(columns).length - 1);
1067
- const totalWidth = (columns) => visibleColumns(columns).reduce((sum, column) => sum + column.width, 0) + visibleGapWidth(columns);
1068
- const currentVariant = (column) => column.variants[column.variantIndex];
1069
- const variantMaxWidth = (column) => {
1070
- const variant = currentVariant(column);
1071
- return variant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(variant.minWidth, variant.maxWidth);
1104
+ //#region src/ink-renderer/columns/root-column.tsx
1105
+ const ROOT_GAP = 1;
1106
+ const visibleWidth = (widths, gap) => {
1107
+ const visible = widths.filter((width) => width > 0);
1108
+ return visible.reduce((sum, width) => sum + width, 0) + Math.max(0, visible.length - 1) * gap;
1072
1109
  };
1073
- const reduceOverflowByShrink = (columns, overflow) => {
1074
- let remaining = overflow;
1075
- const candidates = activeColumns(columns).filter((column) => column.width > currentVariant(column).minWidth).sort((a, b) => currentVariant(a).shrinkResistance - currentVariant(b).shrinkResistance);
1076
- for (const column of candidates) {
1077
- if (remaining <= 0) break;
1078
- const variant = currentVariant(column);
1079
- const reducible = column.width - variant.minWidth;
1080
- if (reducible <= 0) continue;
1081
- const delta = Math.min(reducible, remaining);
1082
- column.width -= delta;
1083
- remaining -= delta;
1084
- }
1085
- return remaining;
1110
+ const ROOT_LAYOUTS = [
1111
+ [
1112
+ DescriptionTreeRootColumn,
1113
+ ProgressRootColumn,
1114
+ ElapsedRootColumn,
1115
+ EtaRootColumn
1116
+ ],
1117
+ [
1118
+ DescriptionPlainRootColumn,
1119
+ ProgressRootColumn,
1120
+ ElapsedRootColumn,
1121
+ EtaRootColumn
1122
+ ],
1123
+ [
1124
+ DescriptionPlainRootColumn,
1125
+ ProgressPercentRootColumn,
1126
+ ElapsedRootColumn,
1127
+ EtaRootColumn
1128
+ ],
1129
+ [
1130
+ DescriptionPlainRootColumn,
1131
+ ProgressPercentRootColumn,
1132
+ ElapsedRootColumn
1133
+ ],
1134
+ [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1135
+ [DescriptionCompactRootColumn, ProgressPercentRootColumn],
1136
+ [DescriptionCompactRootColumn],
1137
+ [DescriptionSpinnerRootColumn]
1138
+ ];
1139
+ const ROOT_COLUMNS = [...new Map(ROOT_LAYOUTS.flat().map((column) => [column.id, column])).values()];
1140
+ const measureColumns = (frame) => new Map(ROOT_COLUMNS.flatMap((definition) => {
1141
+ const column = definition.build(frame);
1142
+ if (column === void 0) return [];
1143
+ const measure = column.measure;
1144
+ return [[definition.id, {
1145
+ definition,
1146
+ id: definition.id,
1147
+ measure,
1148
+ preferredWidth: measure.preferred,
1149
+ commitStickyWidth: column.commitStickyWidth,
1150
+ render: column.render
1151
+ }]];
1152
+ }));
1153
+ const candidateRootLayouts = (columnsById) => {
1154
+ const hasProgress = columnsById.has(ProgressRootColumn.id);
1155
+ const hasPercentProgress = columnsById.has(ProgressPercentRootColumn.id);
1156
+ const hasEta = columnsById.has(EtaRootColumn.id);
1157
+ return [
1158
+ ...hasProgress && hasEta ? [[
1159
+ DescriptionTreeRootColumn,
1160
+ ProgressRootColumn,
1161
+ ElapsedRootColumn,
1162
+ EtaRootColumn
1163
+ ], [
1164
+ DescriptionPlainRootColumn,
1165
+ ProgressRootColumn,
1166
+ ElapsedRootColumn,
1167
+ EtaRootColumn
1168
+ ]] : [],
1169
+ ...hasProgress && !hasEta ? [[
1170
+ DescriptionTreeRootColumn,
1171
+ ProgressRootColumn,
1172
+ ElapsedRootColumn
1173
+ ], [
1174
+ DescriptionPlainRootColumn,
1175
+ ProgressRootColumn,
1176
+ ElapsedRootColumn
1177
+ ]] : [],
1178
+ ...hasPercentProgress && hasEta ? [[
1179
+ DescriptionPlainRootColumn,
1180
+ ProgressPercentRootColumn,
1181
+ ElapsedRootColumn,
1182
+ EtaRootColumn
1183
+ ]] : [],
1184
+ ...hasPercentProgress ? [
1185
+ [
1186
+ DescriptionPlainRootColumn,
1187
+ ProgressPercentRootColumn,
1188
+ ElapsedRootColumn
1189
+ ],
1190
+ [DescriptionPlainRootColumn, ProgressPercentRootColumn],
1191
+ [DescriptionCompactRootColumn, ProgressPercentRootColumn]
1192
+ ] : [],
1193
+ ...!hasProgress && !hasPercentProgress ? [
1194
+ [DescriptionTreeRootColumn, ElapsedRootColumn],
1195
+ [DescriptionPlainRootColumn, ElapsedRootColumn],
1196
+ [DescriptionPlainRootColumn]
1197
+ ] : [],
1198
+ [DescriptionCompactRootColumn],
1199
+ [DescriptionSpinnerRootColumn]
1200
+ ];
1086
1201
  };
1087
- const nextDemoteCandidate = (columns) => activeColumns(columns).filter((column) => column.variantIndex + 1 < column.variants.length).sort((a, b) => currentVariant(a).demoteResistance - currentVariant(b).demoteResistance)[0];
1088
- const applyDemote = (column) => {
1089
- const nextVariant = column.variants[column.variantIndex + 1];
1090
- column.variantIndex += 1;
1091
- const nextMaxWidth = nextVariant.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(nextVariant.minWidth, nextVariant.maxWidth);
1092
- column.width = Math.max(nextVariant.minWidth, Math.min(column.width, nextVariant.idealWidth, nextMaxWidth));
1202
+ const resolveRootLayouts = (columnsById) => {
1203
+ const resolveLayout = (definitions) => {
1204
+ const columns = [];
1205
+ for (const definition of definitions) {
1206
+ const column = columnsById.get(definition.id);
1207
+ if (column === void 0) return;
1208
+ columns.push(column);
1209
+ }
1210
+ return columns;
1211
+ };
1212
+ return candidateRootLayouts(columnsById).map(resolveLayout).filter((columns) => columns !== void 0);
1093
1213
  };
1094
- const nextHideCandidate = (columns) => activeColumns(columns).filter((column) => column.spec.canHide).sort((a, b) => currentVariant(a).hideResistance - currentVariant(b).hideResistance)[0];
1095
- const applyHide = (column) => {
1096
- column.hidden = true;
1097
- column.width = 0;
1214
+ const minimumWidthForSet = (columns) => visibleWidth(columns.map((column) => column.measure.min), ROOT_GAP);
1215
+ const selectColumnSet = (columnSets, terminalColumns) => {
1216
+ if (columnSets.length === 0) return [];
1217
+ if (terminalColumns === void 0) return columnSets[0] ?? [];
1218
+ return columnSets.find((columns) => minimumWidthForSet(columns) <= terminalColumns) ?? columnSets.at(-1) ?? [];
1098
1219
  };
1099
- const distributeGrowth = (columns, extra) => {
1100
- if (extra <= 0) return;
1101
- let remaining = extra;
1102
- while (remaining > 0) {
1103
- const candidates = activeColumns(columns).filter((column) => column.spec.grow > 0 && column.width < variantMaxWidth(column));
1104
- if (candidates.length === 0) break;
1105
- const growSum = candidates.reduce((sum, column) => sum + Math.max(0, column.spec.grow), 0);
1106
- if (growSum <= 0) break;
1107
- let distributed = 0;
1108
- for (const column of candidates) {
1109
- const weight = Math.max(0, column.spec.grow);
1110
- if (weight <= 0) continue;
1111
- const headroom = Math.max(0, variantMaxWidth(column) - column.width);
1112
- if (headroom <= 0) continue;
1113
- const share = Math.min(headroom, Math.floor(remaining * weight / growSum));
1114
- if (share <= 0) continue;
1115
- column.width += share;
1116
- distributed += share;
1117
- }
1118
- if (distributed === 0) {
1119
- for (const column of candidates) {
1120
- if (remaining <= 0) break;
1121
- if (Math.max(0, variantMaxWidth(column) - column.width) <= 0) continue;
1122
- column.width += 1;
1123
- distributed += 1;
1124
- remaining -= 1;
1125
- }
1126
- if (distributed === 0) break;
1220
+ const preferredWidthsForSet = (columns) => columns.map((column) => Math.max(column.measure.min, column.preferredWidth));
1221
+ const nextDistinctWidth = (entries, widest) => entries.find((entry) => entry.width < widest)?.width;
1222
+ const reduceOverflowRichStyle = (widths, minimums, targetWidth) => {
1223
+ let overflow = visibleWidth(widths, ROOT_GAP) - targetWidth;
1224
+ while (overflow > 0) {
1225
+ const shrinkable = widths.map((width, index) => ({
1226
+ width,
1227
+ index,
1228
+ minimum: minimums[index] ?? width
1229
+ })).filter(({ width, minimum }) => width > minimum).sort((left, right) => right.width - left.width || left.index - right.index);
1230
+ if (shrinkable.length === 0) break;
1231
+ const widest = shrinkable[0].width;
1232
+ const cohort = shrinkable.filter(({ width }) => width === widest);
1233
+ const nextWidth = nextDistinctWidth(shrinkable, widest);
1234
+ const maxUniformDrop = widest - Math.max(nextWidth ?? 0, ...cohort.map(({ minimum }) => minimum));
1235
+ const uniformDrop = Math.min(maxUniformDrop, Math.floor(overflow / cohort.length));
1236
+ if (uniformDrop > 0) {
1237
+ for (const { index } of cohort) widths[index] = widths[index] - uniformDrop;
1238
+ overflow -= uniformDrop * cohort.length;
1127
1239
  continue;
1128
1240
  }
1129
- remaining -= distributed;
1130
- }
1131
- };
1132
- const planColumns = ({ context, baselineWidth, columns }) => {
1133
- const mutable = columns.flatMap((column) => {
1134
- if (column.variants.length === 0) return [];
1135
- const first = column.variants[0];
1136
- return [{
1137
- spec: column,
1138
- variants: column.variants,
1139
- variantIndex: 0,
1140
- width: Math.max(first.minWidth, Math.min(first.idealWidth, first.maxWidth === void 0 ? Number.POSITIVE_INFINITY : Math.max(first.minWidth, first.maxWidth))),
1141
- hidden: false
1142
- }];
1143
- });
1144
- if (mutable.length === 0) return {
1145
- rowWidth: 0,
1146
- columns: []
1147
- };
1148
- const naturalWidth = totalWidth(mutable);
1149
- const clampedTerminal = context.terminalColumns === void 0 ? void 0 : clampInt(context.terminalColumns, 1, Number.POSITIVE_INFINITY);
1150
- const baselineTarget = Math.max(1, Math.max(baselineWidth, naturalWidth));
1151
- const target = clampedTerminal === void 0 ? baselineTarget : Math.min(baselineTarget, clampedTerminal);
1152
- if (naturalWidth < target) distributeGrowth(mutable, target - naturalWidth);
1153
- else if (naturalWidth > target) {
1154
- let overflow = naturalWidth - target;
1155
- let progressed = true;
1156
- while (overflow > 0 && progressed) {
1157
- const before = overflow;
1158
- overflow = reduceOverflowByShrink(mutable, overflow);
1241
+ let changed = false;
1242
+ for (const { index, minimum } of cohort) {
1159
1243
  if (overflow <= 0) break;
1160
- const demoteCandidate = nextDemoteCandidate(mutable);
1161
- const hideCandidate = nextHideCandidate(mutable);
1162
- if (demoteCandidate === void 0 && hideCandidate === void 0) {
1163
- progressed = before !== overflow;
1164
- continue;
1165
- }
1166
- if ((demoteCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(demoteCandidate).demoteResistance) <= (hideCandidate === void 0 ? Number.POSITIVE_INFINITY : currentVariant(hideCandidate).hideResistance) && demoteCandidate !== void 0) applyDemote(demoteCandidate);
1167
- else if (hideCandidate !== void 0) applyHide(hideCandidate);
1168
- overflow = Math.max(0, totalWidth(mutable) - target);
1169
- progressed = true;
1244
+ if (widths[index] <= minimum) continue;
1245
+ widths[index] = widths[index] - 1;
1246
+ overflow -= 1;
1247
+ changed = true;
1170
1248
  }
1171
- const compactedWidth = totalWidth(mutable);
1172
- if (compactedWidth < target) distributeGrowth(mutable, target - compactedWidth);
1249
+ if (!changed) break;
1173
1250
  }
1174
- const visible = visibleColumns(mutable);
1175
- return {
1176
- rowWidth: totalWidth(mutable),
1177
- columns: visible.map((column) => {
1178
- const variant = currentVariant(column);
1179
- return {
1180
- id: column.spec.id,
1181
- variantId: variant.id,
1182
- width: column.width,
1183
- renderCell: variant.renderCell
1184
- };
1185
- })
1186
- };
1251
+ return widths;
1187
1252
  };
1188
-
1189
- //#endregion
1190
- //#region src/ink-renderer/columns/layout.tsx
1191
- /**
1192
- * Frame planning algorithm (per render tick):
1193
- *
1194
- * 1. Ask each column module to produce a logical column spec (variants + measured widths).
1195
- * 2. Apply shared resistance rules (shrink / demote / hide) to those variants.
1196
- * 3. Feed resolved columns into `planColumns`.
1197
- * 4. `planColumns` shrinks, demotes, and hides until the row fits target width.
1198
- * 5. Render rows with the selected variant and width for each visible column.
1199
- */
1200
- const BASELINE_ROW_WIDTH = 150;
1201
- const DEFAULT_RESISTANCE = 1e3;
1202
- const variantOrderKey = ({ columnId, variantId }) => `${columnId}:${variantId}`;
1203
- const SHRINK_ORDER = [
1204
- {
1205
- columnId: "eta",
1206
- variantId: "prefixed"
1207
- },
1208
- {
1209
- columnId: "elapsed",
1210
- variantId: "stable"
1211
- },
1212
- {
1213
- columnId: "eta",
1214
- variantId: "duration"
1215
- },
1216
- {
1217
- columnId: "eta",
1218
- variantId: "primary"
1219
- },
1220
- {
1221
- columnId: "bar",
1222
- variantId: "compact"
1223
- },
1224
- {
1225
- columnId: "bar",
1226
- variantId: "full"
1227
- },
1228
- {
1229
- columnId: "amount",
1230
- variantId: "text"
1231
- },
1232
- {
1233
- columnId: "elapsed",
1234
- variantId: "compact"
1235
- },
1236
- {
1237
- columnId: "description",
1238
- variantId: "tree"
1239
- },
1240
- {
1241
- columnId: "amount",
1242
- variantId: "detailed"
1243
- },
1244
- {
1245
- columnId: "amount",
1246
- variantId: "processed"
1247
- },
1248
- {
1249
- columnId: "description",
1250
- variantId: "plain"
1251
- }
1252
- ];
1253
- const DEMOTE_ORDER = [
1254
- {
1255
- columnId: "description",
1256
- variantId: "tree"
1257
- },
1258
- {
1259
- columnId: "eta",
1260
- variantId: "prefixed"
1261
- },
1262
- {
1263
- columnId: "eta",
1264
- variantId: "duration"
1265
- },
1266
- {
1267
- columnId: "elapsed",
1268
- variantId: "stable"
1269
- },
1270
- {
1271
- columnId: "bar",
1272
- variantId: "full"
1273
- },
1274
- {
1275
- columnId: "amount",
1276
- variantId: "detailed"
1277
- }
1278
- ];
1279
- const HIDE_ORDER = [
1280
- {
1281
- columnId: "eta",
1282
- variantId: "primary"
1283
- },
1284
- {
1285
- columnId: "eta",
1286
- variantId: "prefixed"
1287
- },
1288
- {
1289
- columnId: "eta",
1290
- variantId: "duration"
1291
- },
1292
- {
1293
- columnId: "bar",
1294
- variantId: "full"
1295
- },
1296
- {
1297
- columnId: "bar",
1298
- variantId: "compact"
1299
- },
1300
- {
1301
- columnId: "amount",
1302
- variantId: "text"
1303
- },
1304
- {
1305
- columnId: "amount",
1306
- variantId: "detailed"
1307
- },
1308
- {
1309
- columnId: "amount",
1310
- variantId: "processed"
1311
- }
1312
- ];
1313
- const toResistanceLookup = (order) => new Map(order.map((item, index) => [variantOrderKey(item), index + 1]));
1314
- const shrinkResistanceLookup = toResistanceLookup(SHRINK_ORDER);
1315
- const demoteResistanceLookup = toResistanceLookup(DEMOTE_ORDER);
1316
- const hideResistanceLookup = toResistanceLookup(HIDE_ORDER);
1317
- const resistanceFor = (lookup, columnId, variantId) => lookup.get(`${columnId}:${variantId}`) ?? DEFAULT_RESISTANCE;
1318
- const buildColumns = (context, isTTY) => {
1319
- return resolveColumnSpecs([
1320
- createDescriptionColumnSpec(context, isTTY),
1321
- createBarColumnSpec(context, isTTY),
1322
- createAmountColumnSpec(context),
1323
- createElapsedColumnSpec(context, isTTY),
1324
- createEtaColumnSpec(context, isTTY)
1325
- ].filter((spec) => spec !== void 0), {
1326
- shrink: (columnId, variantId) => resistanceFor(shrinkResistanceLookup, columnId, variantId),
1327
- demote: (columnId, variantId) => resistanceFor(demoteResistanceLookup, columnId, variantId),
1328
- hide: (columnId, variantId) => resistanceFor(hideResistanceLookup, columnId, variantId)
1329
- });
1253
+ const widthForSelectedSet = (columns, targetWidth) => {
1254
+ if (columns.length === 0) return [];
1255
+ if (targetWidth === void 0) return preferredWidthsForSet(columns);
1256
+ const minimums = columns.map((column) => column.measure.min);
1257
+ const widths = preferredWidthsForSet(columns);
1258
+ if (visibleWidth(widths, ROOT_GAP) <= targetWidth) return widths;
1259
+ return reduceOverflowRichStyle(widths, minimums, targetWidth);
1330
1260
  };
1331
- const computeFrameLayout = (rows, now, tick, terminalColumns, isTTY) => {
1332
- const context = {
1333
- rows,
1334
- now,
1335
- tick,
1336
- terminalColumns
1337
- };
1338
- return planColumns({
1339
- context,
1340
- columns: buildColumns(context, isTTY),
1341
- baselineWidth: hasDeterminateRows(rows) ? BASELINE_ROW_WIDTH : 1
1342
- });
1261
+ const emptyRootColumn = (stickyWidths) => {
1262
+ stickyWidths.clear();
1263
+ return { render: () => null };
1343
1264
  };
1344
-
1345
- //#endregion
1346
- //#region src/ink-renderer/view/task-row.tsx
1347
- const TaskRow = ({ row, layout }) => {
1348
- return /* @__PURE__ */ jsx(Box, {
1265
+ const RootColumn = (rows, now, tick, terminalColumns, stickyWidths = /* @__PURE__ */ new Map()) => {
1266
+ if (rows.length === 0) return emptyRootColumn(stickyWidths);
1267
+ const selectedColumns = selectColumnSet(resolveRootLayouts(measureColumns(createRenderFrame(rows, now, tick, stickyWidths))), terminalColumns);
1268
+ for (const column of selectedColumns) column.commitStickyWidth?.();
1269
+ const columns = widthForSelectedSet(selectedColumns, terminalColumns === void 0 ? visibleWidth(selectedColumns.map((column) => column.preferredWidth), ROOT_GAP) : terminalColumns).map((width, index) => ({
1270
+ id: selectedColumns[index].id,
1271
+ width,
1272
+ render: selectedColumns[index].render
1273
+ })).filter((column) => column.width > 0);
1274
+ const taskIds = rows.map((row) => row.task.id);
1275
+ const rowWidth = visibleWidth(columns.map((column) => column.width), ROOT_GAP);
1276
+ return { render: () => /* @__PURE__ */ jsx(Box, {
1349
1277
  flexDirection: "row",
1350
- minWidth: layout.rowWidth,
1351
- children: layout.columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1278
+ minWidth: rowWidth,
1279
+ children: columns.map((column, index) => /* @__PURE__ */ jsx(Box, {
1280
+ flexDirection: "column",
1352
1281
  width: column.width,
1353
- flexShrink: column.id === "description" ? 1 : 0,
1354
- marginRight: index < layout.columns.length - 1 ? 1 : 0,
1355
- children: column.renderCell(row, column.width)
1282
+ marginRight: index < columns.length - 1 ? ROOT_GAP : 0,
1283
+ children: taskIds.map((taskId) => /* @__PURE__ */ jsx(Box, {
1284
+ width: column.width,
1285
+ height: 1,
1286
+ children: column.render(taskId, column.width)
1287
+ }, taskId))
1356
1288
  }, column.id))
1357
- });
1358
- };
1359
-
1360
- //#endregion
1361
- //#region src/ink-renderer/view/progress-view.tsx
1362
- const ProgressView = ({ rows, now, tick, isTTY, terminalColumns }) => {
1363
- const layout = computeFrameLayout(rows, now, tick, terminalColumns, isTTY);
1364
- return /* @__PURE__ */ jsx(Box, {
1365
- flexDirection: "column",
1366
- children: rows.map((row) => /* @__PURE__ */ jsx(TaskRow, {
1367
- row,
1368
- layout
1369
- }, row.task.id))
1370
- });
1289
+ }) };
1371
1290
  };
1372
1291
 
1373
1292
  //#endregion
@@ -1407,17 +1326,12 @@ const useSpinnerClock = (active, intervalMillis) => {
1407
1326
  //#region src/ink-renderer/view/render-root.tsx
1408
1327
  const SPINNER_INTERVAL_MILLIS = 100;
1409
1328
  const NOW_INTERVAL_MILLIS = 1e3;
1410
- const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
1329
+ const ProgressRoot = ({ store, getTerminalColumns }) => {
1411
1330
  const snapshot = useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
1412
1331
  const tick = useSpinnerClock(snapshot.hasRunningTasks, SPINNER_INTERVAL_MILLIS);
1413
1332
  const now = useNowClock(snapshot.hasRunningTasks, NOW_INTERVAL_MILLIS);
1414
- return /* @__PURE__ */ jsx(ProgressView, {
1415
- rows: snapshot.rows,
1416
- now,
1417
- tick,
1418
- isTTY,
1419
- terminalColumns: getTerminalColumns()
1420
- });
1333
+ const stickyWidths = useRef(/* @__PURE__ */ new Map());
1334
+ return RootColumn(snapshot.rows, now, tick, getTerminalColumns(), stickyWidths.current).render();
1421
1335
  };
1422
1336
 
1423
1337
  //#endregion
@@ -1425,7 +1339,6 @@ const ProgressRoot = ({ store, isTTY, getTerminalColumns }) => {
1425
1339
  const MAX_FPS = 12;
1426
1340
  const makeDefaultInkRenderer = () => ({ run: (store, stdio, isTTY) => Effect.sync(() => render(/* @__PURE__ */ jsx(ProgressRoot, {
1427
1341
  store,
1428
- isTTY,
1429
1342
  getTerminalColumns: () => isTTY ? stdio.stderr.columns : void 0
1430
1343
  }), {
1431
1344
  stdout: stdio.stdout,