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 +89 -66
- package/package.json +1 -1
- package/src/api.ts +57 -21
- package/src/index.ts +0 -1
- package/src/renderer/ansi.ts +138 -0
- package/src/renderer/tree.ts +68 -0
- package/src/renderer/types.ts +67 -0
- package/src/renderer.ts +747 -719
- package/src/runtime.ts +23 -42
- package/src/types.ts +27 -7
- package/src/theme.ts +0 -87
package/README.md
CHANGED
|
@@ -25,7 +25,9 @@
|
|
|
25
25
|
bun add effective-progress
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
-
-
|
|
103
|
-
-
|
|
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
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
|
-
|
|
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
|
-
|
|
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"> &
|
|
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:
|
|
87
|
+
options: TaskOptions,
|
|
55
88
|
): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
56
89
|
<A, E, R>(
|
|
57
|
-
options:
|
|
90
|
+
options: TaskOptions,
|
|
58
91
|
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
59
|
-
} = dual(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
@@ -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
|
+
}
|