effective-progress 0.4.1 → 0.4.2

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
@@ -25,7 +25,9 @@
25
25
  bun add effective-progress
26
26
  ```
27
27
 
28
- This shows the simplest usage: iterate 100 items with a single progress bar.
28
+ ## Usage
29
+
30
+ This shows the simplest usage: iterate items with a single progress bar.
29
31
 
30
32
  ```ts
31
33
  import { Console, Effect } from "effect";
@@ -46,7 +48,7 @@ Effect.runPromise(program);
46
48
 
47
49
  <img alt="Basic example output" src="docs/images/basic.gif" width="600" />
48
50
 
49
- ## Nested example
51
+ ### Nested example
50
52
 
51
53
  Run:
52
54
 
@@ -77,30 +79,29 @@ Effect.runPromise(program);
77
79
 
78
80
  <img alt="Nested example output" src="docs/images/nesting.gif" width="600" />
79
81
 
80
- ## Other examples
82
+ ### Other examples
81
83
 
82
84
  - `examples/simpleExample.ts` - low-boilerplate real-world flow
83
85
  - `examples/advancedExample.ts` - full API usage with custom config and manual task control
84
86
  - `examples/showcase.ts` - nested concurrent tasks, spinner workloads, and mixed Effect/Console logging
85
87
  - `examples/performance.ts` - stress-style run with high log volume and deeply nested progress updates
86
- - `examples/themeDepthPalette.ts` - depth-aware nested theming via `Theme.depthPalette`
87
- - `examples/twoLineWidthCap.ts` - two-line determinate layout with `maxTaskWidth` clamping
88
- - `examples/customColorStage.ts` - override `ColorStage` to emit plain (no ANSI) frame output
89
88
 
90
- ## Log retention
89
+ ## Configuration
90
+
91
+ ### Log retention
91
92
 
92
93
  - `maxLogLines` on `RendererConfig` controls in-memory log retention.
93
94
  - Omitted or set to `0` means no log history is kept in memory.
94
95
  - `maxLogLines > 0` keeps only the latest `N` log lines in memory.
95
96
 
96
- ## Configuring renderer and progress bars
97
+ ### Configuring renderer and progress bars
97
98
 
98
99
  Configure global renderer behavior once, and a global base progress bar style:
99
100
 
100
101
  Defaults:
101
102
 
102
- - determinate layout: `single-line`
103
- - max task width cap: unset (uses terminal width)
103
+ - columns: `DescriptionColumn`, `BarColumn`, `AmountColumn`, `ElapsedColumn`, `EtaColumn`
104
+ - total progress width: `80`
104
105
  - bar width: `40`
105
106
 
106
107
  ```ts
@@ -109,6 +110,7 @@ import * as Progress from "effective-progress";
109
110
 
110
111
  const configured = program.pipe(
111
112
  Effect.provideService(Progress.RendererConfig, {
113
+ width: 80,
112
114
  maxLogLines: 12,
113
115
  nonTtyUpdateStep: 2,
114
116
  }),
@@ -120,6 +122,49 @@ const configured = program.pipe(
120
122
  Effect.runPromise(configured);
121
123
  ```
122
124
 
125
+ You can customize column order/content Rich-style by providing a `columns` array:
126
+
127
+ ```ts
128
+ const configured = program.pipe(
129
+ Effect.provideService(Progress.RendererConfig, {
130
+ columns: [
131
+ Progress.DescriptionColumn.Default(),
132
+ Progress.BarColumn.make({ track: Progress.Track.fr(1) }),
133
+ Progress.AmountColumn.Default(),
134
+ "•",
135
+ Progress.ElapsedColumn.Default(),
136
+ "•",
137
+ Progress.EtaColumn.Default(),
138
+ ],
139
+ }),
140
+ );
141
+ ```
142
+
143
+ For full terminal width rendering, set:
144
+
145
+ ```ts
146
+ Effect.provideService(Progress.RendererConfig, {
147
+ width: "fullwidth",
148
+ });
149
+ ```
150
+
151
+ Description-specific caps should be configured on `DescriptionColumn` (for example `DescriptionColumn.make({ maxWidth: 40 })`) rather than globally.
152
+
153
+ Per top-level call, you can override render config via helper APIs:
154
+
155
+ ```ts
156
+ const run = Progress.task(effect, {
157
+ description: "work",
158
+ render: {
159
+ columns: [Progress.DescriptionColumn.Default(), "|", Progress.AmountColumn.Default()],
160
+ },
161
+ });
162
+
163
+ const wrapped = Progress.withRenderConfig(run, {
164
+ columns: [Progress.DescriptionColumn.Default()],
165
+ });
166
+ ```
167
+
123
168
  Task-level `progressbar` config is optional and inherits from its parent task (or from global `ProgressBarConfig` for root tasks):
124
169
 
125
170
  ```ts
@@ -133,6 +178,40 @@ yield *
133
178
  });
134
179
  ```
135
180
 
181
+ ## Manual task control
182
+
183
+ For manual usage, `task` captures logs implicitly and provides the current `Task` context:
184
+
185
+ ```ts
186
+ const program = Progress.task(
187
+ Effect.gen(function* () {
188
+ const currentTask = yield* Progress.Task;
189
+ yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
190
+ yield* Effect.sleep("1 second");
191
+ }),
192
+ { description: "Manual task" },
193
+ );
194
+ ```
195
+
196
+ ## Column customization
197
+
198
+ Built-in columns are exported as classes with `Default()` and `make()` helpers.
199
+ You can also pass your own objects/classes implementing `ProgressColumn`:
200
+
201
+ ```ts
202
+ const CustomColumn: Progress.ProgressColumn = {
203
+ id: "custom",
204
+ render: () => "extra",
205
+ };
206
+
207
+ const program = Progress.task(myEffect, {
208
+ description: "Work",
209
+ render: {
210
+ columns: [Progress.DescriptionColumn.Default(), Progress.BarColumn.Default(), CustomColumn],
211
+ },
212
+ });
213
+ ```
214
+
136
215
  ## Terminal service and mocking
137
216
 
138
217
  `effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
@@ -162,62 +241,6 @@ const program = Progress.task(Effect.sleep("100 millis"), { description: "work"
162
241
  );
163
242
  ```
164
243
 
165
- ## Manual task control
166
-
167
- For manual usage, `task` captures logs implicitly and provides the current `Task` context:
168
-
169
- ```ts
170
- const program = Progress.task(
171
- Effect.gen(function* () {
172
- const currentTask = yield* Progress.Task;
173
- yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
174
- yield* Effect.sleep("1 second");
175
- }),
176
- { description: "Manual task" },
177
- );
178
- ```
179
-
180
- ## Themes and render stages
181
-
182
- Coloring is configured through the `Theme` service.
183
-
184
- ```ts
185
- import chalk from "chalk";
186
- import { Effect } from "effect";
187
- import * as Progress from "effective-progress";
188
-
189
- const program = Progress.task(myEffect, { description: "Work" }).pipe(
190
- Effect.provideService(
191
- Progress.Theme,
192
- Progress.Theme.of({
193
- styles: {
194
- plain: (text) => text,
195
- barFill: chalk.hex("#00b894"),
196
- barEmpty: chalk.white.dim,
197
- barBracket: chalk.rgb(180, 190, 210),
198
- spinner: chalk.ansi256(214),
199
- statusDone: chalk.greenBright,
200
- statusFailed: chalk.redBright.bold,
201
- text: chalk.white,
202
- units: chalk.whiteBright.bold,
203
- eta: chalk.gray,
204
- elapsed: chalk.gray,
205
- treeConnector: chalk.gray,
206
- },
207
- depthPalette: (depth, role) => (role === "text" && depth > 0 ? chalk.cyanBright : undefined),
208
- }),
209
- ),
210
- );
211
- ```
212
-
213
- Render internals are split into overrideable stages:
214
-
215
- - `BuildStage`: logical rows/cells/segments
216
- - `ShrinkStage`: width fitting/collapse
217
- - `ColorStage`: role -> styled terminal strings
218
-
219
- You can replace any stage with `Effect.provideService(...)` while keeping defaults for the rest.
220
-
221
244
  ## Dependencies & package size
222
245
 
223
246
  This library is designed for CLI workflows, where package size is typically a lower-priority concern. Alongside `effect` though, I will strive to only rely on other high quality packages.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "effective-progress",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
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": {
package/src/api.ts CHANGED
@@ -1,20 +1,49 @@
1
1
  import { Effect, Exit, Option } from "effect";
2
2
  import { dual } from "effect/Function";
3
3
  import type { Concurrency } from "effect/Types";
4
+ import type { PartialDeep } from "type-fest";
4
5
  import { Progress } from "./runtime";
5
- import { Task } from "./types";
6
- import type { AddTaskOptions, TrackOptions } from "./types";
6
+ import { RendererConfig, Task } from "./types";
7
+ import type { AddTaskOptions, RendererConfigShape, TrackOptions } from "./types";
7
8
  import { inferTotal } from "./utils";
8
9
 
9
- const provideProgress = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
10
+ interface RenderOverrideOptions {
11
+ readonly render?: PartialDeep<RendererConfigShape>;
12
+ }
13
+
14
+ const provideProgress = <A, E, R>(
15
+ effect: Effect.Effect<A, E, R>,
16
+ renderConfig: PartialDeep<RendererConfigShape> | undefined,
17
+ ) =>
10
18
  Effect.gen(function* () {
11
19
  const existing = yield* Effect.serviceOption(Progress);
12
20
  if (Option.isSome(existing)) {
13
21
  return yield* Effect.provideService(effect, Progress, existing.value);
14
22
  }
15
- return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
23
+ if (renderConfig === undefined) {
24
+ return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
25
+ }
26
+
27
+ return yield* Effect.scoped(
28
+ effect.pipe(
29
+ Effect.provide(Progress.Default),
30
+ Effect.provideService(RendererConfig, renderConfig),
31
+ ),
32
+ );
16
33
  });
17
34
 
35
+ export const withRenderConfig: {
36
+ <A, E, R>(
37
+ effect: Effect.Effect<A, E, R>,
38
+ config: PartialDeep<RendererConfigShape>,
39
+ ): Effect.Effect<A, E, Exclude<R, RendererConfig>>;
40
+ (
41
+ config: PartialDeep<RendererConfigShape>,
42
+ ): <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, RendererConfig>>;
43
+ } = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, config: PartialDeep<RendererConfigShape>) =>
44
+ Effect.provideService(effect, RendererConfig, config),
45
+ );
46
+
18
47
  export interface EffectExecutionOptions {
19
48
  readonly concurrency?: Concurrency;
20
49
  readonly batching?: boolean | "inherit";
@@ -26,7 +55,9 @@ export interface EffectAllExecutionOptions extends EffectExecutionOptions {
26
55
  readonly mode?: "default" | "validate" | "either";
27
56
  }
28
57
 
29
- export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
58
+ export type AllOptions = Omit<TrackOptions, "total"> &
59
+ EffectAllExecutionOptions &
60
+ RenderOverrideOptions;
30
61
  export type AllReturn<
31
62
  Arg extends
32
63
  | ReadonlyArray<Effect.Effect<any, any, any>>
@@ -46,26 +77,29 @@ export interface ForEachExecutionOptions extends EffectExecutionOptions {
46
77
  readonly discard?: false | undefined;
47
78
  }
48
79
 
49
- export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
80
+ export type ForEachOptions = TrackOptions & ForEachExecutionOptions & RenderOverrideOptions;
81
+
82
+ export type TaskOptions = AddTaskOptions & RenderOverrideOptions;
50
83
 
51
84
  export const task: {
52
85
  <A, E, R>(
53
86
  effect: Effect.Effect<A, E, R>,
54
- options: AddTaskOptions,
87
+ options: TaskOptions,
55
88
  ): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
56
89
  <A, E, R>(
57
- options: AddTaskOptions,
90
+ options: TaskOptions,
58
91
  ): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
59
- } = dual(
60
- 2,
61
- <A, E, R>(effect: Effect.Effect<A, E, R>, options: AddTaskOptions) =>
62
- provideProgress(
63
- Effect.gen(function* () {
64
- const progress = yield* Progress;
65
- return yield* progress.withTask(effect, options);
66
- }),
67
- ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>,
68
- );
92
+ } = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions) => {
93
+ const { render, ...taskOptions } = options;
94
+
95
+ return provideProgress(
96
+ Effect.gen(function* () {
97
+ const progress = yield* Progress;
98
+ return yield* progress.withTask(effect, taskOptions);
99
+ }),
100
+ render,
101
+ ) as Effect.Effect<A, E, Exclude<R, Progress | Task>>;
102
+ });
69
103
 
70
104
  type AllArg =
71
105
  | ReadonlyArray<Effect.Effect<any, any, any>>
@@ -85,14 +119,14 @@ const countEffects = (effects: AllArg): number =>
85
119
  export const all: {
86
120
  <const Arg extends AllArg, O extends EffectAllExecutionOptions>(
87
121
  effects: Arg,
88
- options: Omit<TrackOptions, "total"> & O,
122
+ options: Omit<TrackOptions, "total"> & O & RenderOverrideOptions,
89
123
  ): AllReturn<Arg, O>;
90
- <O extends EffectAllExecutionOptions>(
124
+ <O extends EffectAllExecutionOptions & RenderOverrideOptions>(
91
125
  options: Omit<TrackOptions, "total"> & O,
92
126
  ): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
93
127
  } = dual(
94
128
  2,
95
- <const Arg extends AllArg, O extends EffectAllExecutionOptions>(
129
+ <const Arg extends AllArg, O extends EffectAllExecutionOptions & RenderOverrideOptions>(
96
130
  effects: Arg,
97
131
  options: Omit<TrackOptions, "total"> & O,
98
132
  ) =>
@@ -136,6 +170,7 @@ export const all: {
136
170
  },
137
171
  );
138
172
  }),
173
+ options.render,
139
174
  ) as AllReturn<Arg, O>,
140
175
  );
141
176
 
@@ -195,5 +230,6 @@ export const forEach: {
195
230
  },
196
231
  );
197
232
  }),
233
+ options.render,
198
234
  ) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>,
199
235
  );
package/src/index.ts CHANGED
@@ -2,5 +2,4 @@ export * from "./api";
2
2
  export * from "./renderer";
3
3
  export { Progress } from "./runtime";
4
4
  export * from "./terminal";
5
- export * from "./theme";
6
5
  export * from "./types";
@@ -0,0 +1,138 @@
1
+ import type { CellWrapMode } from "./types";
2
+
3
+ const ESC = String.fromCharCode(27);
4
+ const ANSI_PATTERN = new RegExp(`${ESC}\\[[0-9;?]*[ -/]*[@-~]`, "g");
5
+ const RESET_ANSI = "\x1b[0m";
6
+
7
+ interface AnsiToken {
8
+ readonly kind: "ansi" | "char";
9
+ readonly value: string;
10
+ }
11
+
12
+ const textWidth = (text: string): number => Array.from(text).length;
13
+
14
+ export const stripAnsi = (text: string): string => text.replace(ANSI_PATTERN, "");
15
+
16
+ export const visibleWidth = (text: string): number => textWidth(stripAnsi(text));
17
+
18
+ const tokenizeAnsi = (text: string): ReadonlyArray<AnsiToken> => {
19
+ const tokens: Array<AnsiToken> = [];
20
+ let i = 0;
21
+
22
+ while (i < text.length) {
23
+ if (text[i] === "\x1b" && text[i + 1] === "[") {
24
+ let j = i + 2;
25
+ while (j < text.length) {
26
+ const code = text.charCodeAt(j);
27
+ j += 1;
28
+ if (code >= 0x40 && code <= 0x7e) {
29
+ break;
30
+ }
31
+ }
32
+ tokens.push({ kind: "ansi", value: text.slice(i, j) });
33
+ i = j;
34
+ continue;
35
+ }
36
+
37
+ const codePoint = text.codePointAt(i);
38
+ if (codePoint === undefined) {
39
+ break;
40
+ }
41
+
42
+ const char = String.fromCodePoint(codePoint);
43
+ tokens.push({ kind: "char", value: char });
44
+ i += char.length;
45
+ }
46
+
47
+ return tokens;
48
+ };
49
+
50
+ const fitPlainText = (text: string, width: number, wrapMode: CellWrapMode): string => {
51
+ const target = Math.max(0, Math.floor(width));
52
+ if (target <= 0) {
53
+ return "";
54
+ }
55
+
56
+ const chars = Array.from(text);
57
+ let result = text;
58
+
59
+ if (chars.length > target) {
60
+ if (wrapMode === "ellipsis") {
61
+ result = target === 1 ? "…" : `${chars.slice(0, target - 1).join("")}…`;
62
+ } else {
63
+ result = chars.slice(0, target).join("");
64
+ }
65
+ }
66
+
67
+ const currentWidth = textWidth(result);
68
+ if (currentWidth < target) {
69
+ result += " ".repeat(target - currentWidth);
70
+ }
71
+
72
+ return result;
73
+ };
74
+
75
+ const fitAnsiText = (text: string, width: number, wrapMode: CellWrapMode): string => {
76
+ const target = Math.max(0, Math.floor(width));
77
+ if (target <= 0) {
78
+ return "";
79
+ }
80
+
81
+ const tokens = tokenizeAnsi(text);
82
+ const totalVisible = tokens.reduce((sum, token) => sum + (token.kind === "char" ? 1 : 0), 0);
83
+
84
+ if (totalVisible <= target) {
85
+ const padBy = target - totalVisible;
86
+ return padBy > 0 ? `${text}${" ".repeat(padBy)}` : text;
87
+ }
88
+
89
+ const keepVisible = wrapMode === "ellipsis" ? Math.max(0, target - 1) : target;
90
+ let visible = 0;
91
+ let sawAnsi = false;
92
+ let output = "";
93
+
94
+ for (const token of tokens) {
95
+ if (token.kind === "ansi") {
96
+ sawAnsi = true;
97
+ if (keepVisible > 0 && visible <= keepVisible) {
98
+ output += token.value;
99
+ }
100
+ continue;
101
+ }
102
+
103
+ if (visible >= keepVisible) {
104
+ break;
105
+ }
106
+
107
+ output += token.value;
108
+ visible += 1;
109
+ }
110
+
111
+ if (wrapMode === "ellipsis" && target > 0) {
112
+ output += "…";
113
+ }
114
+
115
+ if (sawAnsi && !output.endsWith(RESET_ANSI)) {
116
+ output += RESET_ANSI;
117
+ }
118
+
119
+ const padBy = target - visibleWidth(output);
120
+ if (padBy > 0) {
121
+ output += " ".repeat(padBy);
122
+ }
123
+
124
+ return output;
125
+ };
126
+
127
+ export const fitRenderedText = (
128
+ text: string,
129
+ width: number,
130
+ wrapMode: CellWrapMode,
131
+ isTTY: boolean,
132
+ ): string => {
133
+ const raw = isTTY ? text : stripAnsi(text);
134
+ if (!isTTY || raw.indexOf("\x1b[") === -1) {
135
+ return fitPlainText(raw, width, wrapMode);
136
+ }
137
+ return fitAnsiText(raw, width, wrapMode);
138
+ };
@@ -0,0 +1,68 @@
1
+ import type { TaskSnapshot } from "../types";
2
+ import type { TaskTreeInfo } from "./types";
3
+
4
+ export interface OrderedTreeTask {
5
+ readonly snapshot: TaskSnapshot;
6
+ readonly depth: number;
7
+ }
8
+
9
+ const treeAncestorPrefix = (tree: TaskTreeInfo): string =>
10
+ tree.ancestorHasNextSibling
11
+ .slice(1)
12
+ .map((hasNext) => (hasNext ? "│ " : " "))
13
+ .join("");
14
+
15
+ export const renderTreePrefix = (tree: TaskTreeInfo): string => {
16
+ if (tree.depth <= 0) {
17
+ return "";
18
+ }
19
+
20
+ const ancestor = treeAncestorPrefix(tree);
21
+ return `${ancestor}${tree.hasNextSibling ? "├─ " : "└─ "}`;
22
+ };
23
+
24
+ export const computeTreeInfo = (
25
+ ordered: ReadonlyArray<OrderedTreeTask>,
26
+ ): ReadonlyArray<OrderedTreeTask & { readonly tree: TaskTreeInfo }> => {
27
+ const hasNextSiblingByIndex: Array<boolean> = Array.from({ length: ordered.length }, () => false);
28
+
29
+ for (let i = 0; i < ordered.length; i++) {
30
+ const depth = ordered[i]!.depth;
31
+ for (let j = i + 1; j < ordered.length; j++) {
32
+ const candidateDepth = ordered[j]!.depth;
33
+ if (candidateDepth < depth) {
34
+ break;
35
+ }
36
+ if (candidateDepth === depth) {
37
+ hasNextSiblingByIndex[i] = true;
38
+ break;
39
+ }
40
+ }
41
+ }
42
+
43
+ const ancestorStateByDepth: Array<boolean> = [];
44
+
45
+ return ordered.map((entry, index) => {
46
+ const depth = entry.depth;
47
+ ancestorStateByDepth.length = depth;
48
+
49
+ const hasChildren =
50
+ index + 1 < ordered.length &&
51
+ ordered[index + 1] !== undefined &&
52
+ ordered[index + 1]!.depth > depth;
53
+
54
+ const tree: TaskTreeInfo = {
55
+ depth,
56
+ hasNextSibling: hasNextSiblingByIndex[index] ?? false,
57
+ hasChildren,
58
+ ancestorHasNextSibling: [...ancestorStateByDepth],
59
+ };
60
+
61
+ ancestorStateByDepth[depth] = hasNextSiblingByIndex[index] ?? false;
62
+
63
+ return {
64
+ ...entry,
65
+ tree,
66
+ };
67
+ });
68
+ };
@@ -0,0 +1,67 @@
1
+ import type { TaskSnapshot } from "../types";
2
+
3
+ /**
4
+ * Defines how a column claims width during the shrink/fit stage.
5
+ */
6
+ export type ColumnTrack =
7
+ | {
8
+ readonly _tag: "Auto";
9
+ }
10
+ | {
11
+ readonly _tag: "Fixed";
12
+ readonly width: number;
13
+ }
14
+ | {
15
+ readonly _tag: "Fraction";
16
+ readonly weight: number;
17
+ };
18
+
19
+ export const Track = {
20
+ auto: (): ColumnTrack => ({ _tag: "Auto" }),
21
+ fixed: (width: number): ColumnTrack => ({
22
+ _tag: "Fixed",
23
+ width: Math.max(0, Math.floor(width)),
24
+ }),
25
+ fr: (weight = 1): ColumnTrack => ({
26
+ _tag: "Fraction",
27
+ weight: Math.max(0.001, weight),
28
+ }),
29
+ } as const;
30
+
31
+ /**
32
+ * Tree relationship metadata for rendering connectors.
33
+ */
34
+ export interface TaskTreeInfo {
35
+ readonly depth: number;
36
+ readonly hasNextSibling: boolean;
37
+ readonly hasChildren: boolean;
38
+ readonly ancestorHasNextSibling: ReadonlyArray<boolean>;
39
+ }
40
+
41
+ export type CellWrapMode = "truncate" | "ellipsis";
42
+
43
+ export interface ProgressColumnContext {
44
+ readonly task: TaskSnapshot;
45
+ readonly depth: number;
46
+ readonly tree: TaskTreeInfo;
47
+ readonly now: number;
48
+ readonly tick: number;
49
+ readonly isTTY: boolean;
50
+ }
51
+
52
+ export interface ProgressColumnVariant {
53
+ readonly measure?: (context: ProgressColumnContext) => number;
54
+ readonly render: (context: ProgressColumnContext, width: number) => string;
55
+ }
56
+
57
+ export interface ProgressColumn {
58
+ readonly id: string;
59
+ readonly track?: ColumnTrack;
60
+ readonly minWidth?: number;
61
+ readonly maxWidth?: number;
62
+ readonly collapsePriority?: number;
63
+ readonly wrapMode?: CellWrapMode;
64
+ readonly measure?: (context: ProgressColumnContext) => number;
65
+ readonly render: (context: ProgressColumnContext, width: number) => string;
66
+ readonly variants?: (context: ProgressColumnContext) => ReadonlyArray<ProgressColumnVariant>;
67
+ }