effective-progress 0.4.0 → 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,27 +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
88
 
87
- ## Log retention
89
+ ## Configuration
90
+
91
+ ### Log retention
88
92
 
89
93
  - `maxLogLines` on `RendererConfig` controls in-memory log retention.
90
94
  - Omitted or set to `0` means no log history is kept in memory.
91
95
  - `maxLogLines > 0` keeps only the latest `N` log lines in memory.
92
96
 
93
- ## Configuring renderer and progress bars
97
+ ### Configuring renderer and progress bars
94
98
 
95
99
  Configure global renderer behavior once, and a global base progress bar style:
96
100
 
97
101
  Defaults:
98
102
 
99
- - determinate layout: `single-line`
100
- - max task width cap: unset (uses terminal width)
103
+ - columns: `DescriptionColumn`, `BarColumn`, `AmountColumn`, `ElapsedColumn`, `EtaColumn`
104
+ - total progress width: `80`
101
105
  - bar width: `40`
102
106
 
103
107
  ```ts
@@ -106,6 +110,7 @@ import * as Progress from "effective-progress";
106
110
 
107
111
  const configured = program.pipe(
108
112
  Effect.provideService(Progress.RendererConfig, {
113
+ width: 80,
109
114
  maxLogLines: 12,
110
115
  nonTtyUpdateStep: 2,
111
116
  }),
@@ -117,6 +122,49 @@ const configured = program.pipe(
117
122
  Effect.runPromise(configured);
118
123
  ```
119
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
+
120
168
  Task-level `progressbar` config is optional and inherits from its parent task (or from global `ProgressBarConfig` for root tasks):
121
169
 
122
170
  ```ts
@@ -130,6 +178,40 @@ yield *
130
178
  });
131
179
  ```
132
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
+
133
215
  ## Terminal service and mocking
134
216
 
135
217
  `effective-progress` now exposes a `ProgressTerminal` service that controls terminal detection and I/O:
@@ -159,62 +241,6 @@ const program = Progress.task(Effect.sleep("100 millis"), { description: "work"
159
241
  );
160
242
  ```
161
243
 
162
- ## Manual task control
163
-
164
- For manual usage, `task` captures logs implicitly and provides the current `Task` context:
165
-
166
- ```ts
167
- const program = Progress.task(
168
- Effect.gen(function* () {
169
- const currentTask = yield* Progress.Task;
170
- yield* Console.log("This log is rendered through progress output", { taskId: currentTask });
171
- yield* Effect.sleep("1 second");
172
- }),
173
- { description: "Manual task" },
174
- );
175
- ```
176
-
177
- ## Themes and render stages
178
-
179
- Coloring is configured through the `Theme` service.
180
-
181
- ```ts
182
- import chalk from "chalk";
183
- import { Effect } from "effect";
184
- import * as Progress from "effective-progress";
185
-
186
- const program = Progress.task(myEffect, { description: "Work" }).pipe(
187
- Effect.provideService(
188
- Progress.Theme,
189
- Progress.Theme.of({
190
- styles: {
191
- plain: (text) => text,
192
- barFill: chalk.hex("#00b894"),
193
- barEmpty: chalk.white.dim,
194
- barBracket: chalk.rgb(180, 190, 210),
195
- spinner: chalk.ansi256(214),
196
- statusDone: chalk.greenBright,
197
- statusFailed: chalk.redBright.bold,
198
- text: chalk.white,
199
- units: chalk.whiteBright.bold,
200
- eta: chalk.gray,
201
- elapsed: chalk.gray,
202
- treeConnector: chalk.gray,
203
- },
204
- depthPalette: (depth, role) => (role === "text" && depth > 0 ? chalk.cyanBright : undefined),
205
- }),
206
- ),
207
- );
208
- ```
209
-
210
- Render internals are split into overrideable stages:
211
-
212
- - `BuildStage`: logical rows/cells/segments
213
- - `ShrinkStage`: width fitting/collapse
214
- - `ColorStage`: role -> styled terminal strings
215
-
216
- You can replace any stage with `Effect.provideService(...)` while keeping defaults for the rest.
217
-
218
244
  ## Dependencies & package size
219
245
 
220
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.0",
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
+ }