effective-progress 0.5.1 → 0.5.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/dist/index.d.mts +179 -0
- package/dist/index.mjs +815 -0
- package/package.json +15 -4
- package/index.ts +0 -1
- package/src/api.ts +0 -197
- package/src/index.ts +0 -4
- package/src/ink-renderer/app.tsx +0 -31
- package/src/ink-renderer/columns/amount-column.tsx +0 -17
- package/src/ink-renderer/columns/bar-column.tsx +0 -31
- package/src/ink-renderer/columns/description-column.tsx +0 -7
- package/src/ink-renderer/columns/elapsed-column.tsx +0 -7
- package/src/ink-renderer/columns/eta-column.tsx +0 -18
- package/src/ink-renderer/columns/index.ts +0 -6
- package/src/ink-renderer/columns/types.ts +0 -11
- package/src/ink-renderer/format.ts +0 -55
- package/src/ink-renderer/index.ts +0 -1
- package/src/ink-renderer/layout.ts +0 -145
- package/src/ink-renderer/model.ts +0 -24
- package/src/ink-renderer/service.tsx +0 -121
- package/src/ink-renderer/task-row.tsx +0 -53
- package/src/ink-renderer/tree.ts +0 -62
- package/src/ink-renderer/types.ts +0 -18
- package/src/runtime.ts +0 -396
- package/src/terminal.ts +0 -61
- package/src/types.ts +0 -143
- package/src/utils.ts +0 -20
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "effective-progress",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.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": {
|
|
@@ -12,13 +12,21 @@
|
|
|
12
12
|
"url": "git+https://github.com/stromseng/effective-progress.git"
|
|
13
13
|
},
|
|
14
14
|
"files": [
|
|
15
|
-
"
|
|
16
|
-
"src",
|
|
15
|
+
"dist",
|
|
17
16
|
"README.md",
|
|
18
17
|
"LICENSE"
|
|
19
18
|
],
|
|
20
19
|
"type": "module",
|
|
21
|
-
"
|
|
20
|
+
"main": "./dist/index.mjs",
|
|
21
|
+
"module": "./dist/index.mjs",
|
|
22
|
+
"types": "./dist/index.d.mts",
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.mts",
|
|
26
|
+
"import": "./dist/index.mjs",
|
|
27
|
+
"default": "./dist/index.mjs"
|
|
28
|
+
}
|
|
29
|
+
},
|
|
22
30
|
"engines": {
|
|
23
31
|
"node": ">=20.12.0"
|
|
24
32
|
},
|
|
@@ -27,6 +35,8 @@
|
|
|
27
35
|
},
|
|
28
36
|
"scripts": {
|
|
29
37
|
"prepare": "effect-language-service patch",
|
|
38
|
+
"build": "tsdown",
|
|
39
|
+
"prepublishOnly": "bun run build",
|
|
30
40
|
"typecheck": "tsc --noEmit",
|
|
31
41
|
"lint": "oxlint .",
|
|
32
42
|
"format": "oxfmt --write .",
|
|
@@ -43,6 +53,7 @@
|
|
|
43
53
|
"effect": "^3.19.17",
|
|
44
54
|
"oxfmt": "^0.32.0",
|
|
45
55
|
"oxlint": "^1.47.0",
|
|
56
|
+
"tsdown": "^0.21.0-beta.2",
|
|
46
57
|
"typescript": "^5"
|
|
47
58
|
},
|
|
48
59
|
"peerDependencies": {
|
package/index.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export * from "./src";
|
package/src/api.ts
DELETED
|
@@ -1,197 +0,0 @@
|
|
|
1
|
-
import { Effect, Exit, Option } from "effect";
|
|
2
|
-
import { dual } from "effect/Function";
|
|
3
|
-
import type { Concurrency } from "effect/Types";
|
|
4
|
-
import { Progress } from "./runtime";
|
|
5
|
-
import { Task } from "./types";
|
|
6
|
-
import type { AddTaskOptions, TrackOptions } from "./types";
|
|
7
|
-
import { inferTotal } from "./utils";
|
|
8
|
-
|
|
9
|
-
const provideProgress = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
|
|
10
|
-
Effect.gen(function* () {
|
|
11
|
-
const existing = yield* Effect.serviceOption(Progress);
|
|
12
|
-
if (Option.isSome(existing)) {
|
|
13
|
-
return yield* Effect.provideService(effect, Progress, existing.value);
|
|
14
|
-
}
|
|
15
|
-
return yield* Effect.scoped(effect.pipe(Effect.provide(Progress.Default)));
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
export interface EffectExecutionOptions {
|
|
19
|
-
readonly concurrency?: Concurrency;
|
|
20
|
-
readonly batching?: boolean | "inherit";
|
|
21
|
-
readonly concurrentFinalizers?: boolean;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface EffectAllExecutionOptions extends EffectExecutionOptions {
|
|
25
|
-
readonly discard?: boolean;
|
|
26
|
-
readonly mode?: "default" | "validate" | "either";
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export type AllOptions = Omit<TrackOptions, "total"> & EffectAllExecutionOptions;
|
|
30
|
-
export type AllReturn<
|
|
31
|
-
Arg extends
|
|
32
|
-
| ReadonlyArray<Effect.Effect<any, any, any>>
|
|
33
|
-
| Record<string, Effect.Effect<any, any, any>>,
|
|
34
|
-
O extends EffectAllExecutionOptions,
|
|
35
|
-
> = [
|
|
36
|
-
[Arg] extends [ReadonlyArray<Effect.Effect<any, any, any>>]
|
|
37
|
-
? Effect.All.ReturnTuple<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
|
|
38
|
-
: [Arg] extends [Record<string, Effect.Effect<any, any, any>>]
|
|
39
|
-
? Effect.All.ReturnObject<Arg, Effect.All.IsDiscard<O>, Effect.All.ExtractMode<O>>
|
|
40
|
-
: never,
|
|
41
|
-
] extends [Effect.Effect<infer A, infer E, infer R>]
|
|
42
|
-
? Effect.Effect<A, E, Exclude<R, Progress | Task>>
|
|
43
|
-
: never;
|
|
44
|
-
|
|
45
|
-
export interface ForEachExecutionOptions extends EffectExecutionOptions {
|
|
46
|
-
readonly discard?: false | undefined;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export type ForEachOptions = TrackOptions & ForEachExecutionOptions;
|
|
50
|
-
|
|
51
|
-
export type TaskOptions = AddTaskOptions;
|
|
52
|
-
|
|
53
|
-
export const task: {
|
|
54
|
-
<A, E, R>(
|
|
55
|
-
effect: Effect.Effect<A, E, R>,
|
|
56
|
-
options: TaskOptions,
|
|
57
|
-
): Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
58
|
-
<A, E, R>(
|
|
59
|
-
options: TaskOptions,
|
|
60
|
-
): (effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, Exclude<R, Progress | Task>>;
|
|
61
|
-
} = dual(2, <A, E, R>(effect: Effect.Effect<A, E, R>, options: TaskOptions) => {
|
|
62
|
-
return 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
|
-
});
|
|
69
|
-
|
|
70
|
-
type AllArg =
|
|
71
|
-
| ReadonlyArray<Effect.Effect<any, any, any>>
|
|
72
|
-
| Record<string, Effect.Effect<any, any, any>>;
|
|
73
|
-
|
|
74
|
-
const wrapEffects = (
|
|
75
|
-
effects: AllArg,
|
|
76
|
-
tap: (effect: Effect.Effect<any, any, any>) => Effect.Effect<any, any, any>,
|
|
77
|
-
): AllArg =>
|
|
78
|
-
Array.isArray(effects)
|
|
79
|
-
? effects.map(tap)
|
|
80
|
-
: Object.fromEntries(Object.entries(effects).map(([k, effect]) => [k, tap(effect)]));
|
|
81
|
-
|
|
82
|
-
const countEffects = (effects: AllArg): number =>
|
|
83
|
-
Array.isArray(effects) ? effects.length : Object.keys(effects).length;
|
|
84
|
-
|
|
85
|
-
export const all: {
|
|
86
|
-
<const Arg extends AllArg, O extends EffectAllExecutionOptions>(
|
|
87
|
-
effects: Arg,
|
|
88
|
-
options: Omit<TrackOptions, "total"> & O,
|
|
89
|
-
): AllReturn<Arg, O>;
|
|
90
|
-
<O extends EffectAllExecutionOptions>(
|
|
91
|
-
options: Omit<TrackOptions, "total"> & O,
|
|
92
|
-
): <const Arg extends AllArg>(effects: Arg) => AllReturn<Arg, O>;
|
|
93
|
-
} = dual(
|
|
94
|
-
2,
|
|
95
|
-
<const Arg extends AllArg, O extends EffectAllExecutionOptions>(
|
|
96
|
-
effects: Arg,
|
|
97
|
-
options: Omit<TrackOptions, "total"> & O,
|
|
98
|
-
) =>
|
|
99
|
-
provideProgress(
|
|
100
|
-
Effect.gen(function* () {
|
|
101
|
-
const progress = yield* Progress;
|
|
102
|
-
return yield* progress.runTask(
|
|
103
|
-
Effect.gen(function* () {
|
|
104
|
-
const taskId = yield* Task;
|
|
105
|
-
const exit = yield* Effect.exit(
|
|
106
|
-
Effect.all(
|
|
107
|
-
wrapEffects(effects, (effect) =>
|
|
108
|
-
Effect.tap(effect, () => progress.advanceTask(taskId, 1)),
|
|
109
|
-
),
|
|
110
|
-
{
|
|
111
|
-
concurrency: options.concurrency,
|
|
112
|
-
batching: options.batching,
|
|
113
|
-
discard: options.discard,
|
|
114
|
-
mode: options.mode,
|
|
115
|
-
concurrentFinalizers: options.concurrentFinalizers,
|
|
116
|
-
},
|
|
117
|
-
),
|
|
118
|
-
);
|
|
119
|
-
|
|
120
|
-
if (Exit.isSuccess(exit)) {
|
|
121
|
-
yield* progress.completeTask(taskId);
|
|
122
|
-
} else {
|
|
123
|
-
yield* progress.failTask(taskId);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return yield* Exit.match(exit, {
|
|
127
|
-
onFailure: Effect.failCause,
|
|
128
|
-
onSuccess: Effect.succeed,
|
|
129
|
-
});
|
|
130
|
-
}),
|
|
131
|
-
{
|
|
132
|
-
description: options.description,
|
|
133
|
-
total: countEffects(effects),
|
|
134
|
-
transient: options.transient,
|
|
135
|
-
},
|
|
136
|
-
);
|
|
137
|
-
}),
|
|
138
|
-
) as AllReturn<Arg, O>,
|
|
139
|
-
);
|
|
140
|
-
|
|
141
|
-
export const forEach: {
|
|
142
|
-
<A, B, E, R>(
|
|
143
|
-
iterable: Iterable<A>,
|
|
144
|
-
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
145
|
-
options: ForEachOptions,
|
|
146
|
-
): Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
147
|
-
<A, B, E, R>(
|
|
148
|
-
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
149
|
-
options: ForEachOptions,
|
|
150
|
-
): (iterable: Iterable<A>) => Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>;
|
|
151
|
-
} = dual(
|
|
152
|
-
3,
|
|
153
|
-
<A, B, E, R>(
|
|
154
|
-
iterable: Iterable<A>,
|
|
155
|
-
f: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
156
|
-
options: ForEachOptions,
|
|
157
|
-
) =>
|
|
158
|
-
provideProgress(
|
|
159
|
-
Effect.gen(function* () {
|
|
160
|
-
const progress = yield* Progress;
|
|
161
|
-
|
|
162
|
-
return yield* progress.runTask(
|
|
163
|
-
Effect.gen(function* () {
|
|
164
|
-
const taskId = yield* Task;
|
|
165
|
-
const exit = yield* Effect.exit(
|
|
166
|
-
Effect.forEach(
|
|
167
|
-
iterable,
|
|
168
|
-
(item, index) => Effect.tap(f(item, index), () => progress.advanceTask(taskId, 1)),
|
|
169
|
-
{
|
|
170
|
-
concurrency: options.concurrency,
|
|
171
|
-
batching: options.batching,
|
|
172
|
-
discard: options.discard,
|
|
173
|
-
concurrentFinalizers: options.concurrentFinalizers,
|
|
174
|
-
},
|
|
175
|
-
),
|
|
176
|
-
);
|
|
177
|
-
|
|
178
|
-
if (Exit.isSuccess(exit)) {
|
|
179
|
-
yield* progress.completeTask(taskId);
|
|
180
|
-
} else {
|
|
181
|
-
yield* progress.failTask(taskId);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return yield* Exit.match(exit, {
|
|
185
|
-
onFailure: Effect.failCause,
|
|
186
|
-
onSuccess: Effect.succeed,
|
|
187
|
-
});
|
|
188
|
-
}),
|
|
189
|
-
{
|
|
190
|
-
description: options.description,
|
|
191
|
-
total: options.total ?? inferTotal(iterable),
|
|
192
|
-
transient: options.transient,
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
}),
|
|
196
|
-
) as Effect.Effect<ReadonlyArray<B>, E, Exclude<R, Progress | Task>>,
|
|
197
|
-
);
|
package/src/index.ts
DELETED
package/src/ink-renderer/app.tsx
DELETED
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Box } from "ink";
|
|
2
|
-
import { computeSharedColumnWidths } from "./layout";
|
|
3
|
-
import { TaskRow } from "./task-row";
|
|
4
|
-
import type { TaskRowModel } from "./types";
|
|
5
|
-
|
|
6
|
-
export interface ProgressAppProps {
|
|
7
|
-
readonly rows: ReadonlyArray<TaskRowModel>;
|
|
8
|
-
readonly now: number;
|
|
9
|
-
readonly tick: number;
|
|
10
|
-
readonly isTTY: boolean;
|
|
11
|
-
readonly terminalColumns?: number;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export const ProgressApp = ({ rows, now, tick, isTTY, terminalColumns }: ProgressAppProps) => {
|
|
15
|
-
const widths = computeSharedColumnWidths(rows, now, tick, terminalColumns);
|
|
16
|
-
|
|
17
|
-
return (
|
|
18
|
-
<Box flexDirection="column">
|
|
19
|
-
{rows.map((row) => (
|
|
20
|
-
<TaskRow
|
|
21
|
-
key={row.task.id as number}
|
|
22
|
-
row={row}
|
|
23
|
-
now={now}
|
|
24
|
-
tick={tick}
|
|
25
|
-
isTTY={isTTY}
|
|
26
|
-
widths={widths}
|
|
27
|
-
/>
|
|
28
|
-
))}
|
|
29
|
-
</Box>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { Text } from "ink";
|
|
2
|
-
import { formatAmount } from "../format";
|
|
3
|
-
import type { ColumnProps } from "./types";
|
|
4
|
-
|
|
5
|
-
export const AmountColumn = ({ task, tick }: ColumnProps) => {
|
|
6
|
-
const text = formatAmount(task, tick);
|
|
7
|
-
const color =
|
|
8
|
-
task.status === "failed"
|
|
9
|
-
? "red"
|
|
10
|
-
: task.status === "done"
|
|
11
|
-
? "green"
|
|
12
|
-
: task.units._tag === "DeterminateTaskUnits"
|
|
13
|
-
? "whiteBright"
|
|
14
|
-
: "yellow";
|
|
15
|
-
|
|
16
|
-
return <Text color={color}>{text}</Text>;
|
|
17
|
-
};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { Text } from "ink";
|
|
2
|
-
import type { ColumnProps } from "./types";
|
|
3
|
-
|
|
4
|
-
const clamp = (value: number, minimum: number, maximum: number): number =>
|
|
5
|
-
Math.min(Math.max(value, minimum), maximum);
|
|
6
|
-
|
|
7
|
-
export interface BarColumnProps extends ColumnProps {
|
|
8
|
-
readonly width: number;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export const BarColumn = ({ task, width }: BarColumnProps) => {
|
|
12
|
-
if (task.units._tag !== "DeterminateTaskUnits") {
|
|
13
|
-
return <Text />;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const barWidth = Math.max(1, Math.floor(width));
|
|
17
|
-
const safeTotal = Math.max(1, task.units.total);
|
|
18
|
-
const ratio = task.status === "done" ? 1 : clamp(task.units.completed / safeTotal, 0, 1);
|
|
19
|
-
const filled = Math.round(barWidth * ratio);
|
|
20
|
-
const empty = Math.max(0, barWidth - filled);
|
|
21
|
-
const bar = `${"━".repeat(filled)}${"─".repeat(empty)}`;
|
|
22
|
-
|
|
23
|
-
const color =
|
|
24
|
-
task.status === "failed" ? "red" : task.status === "done" ? "green" : "blue";
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<Text wrap="truncate-end" color={color}>
|
|
28
|
-
{bar}
|
|
29
|
-
</Text>
|
|
30
|
-
);
|
|
31
|
-
};
|
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
import { Text } from "ink";
|
|
2
|
-
import { renderTreePrefix } from "../tree";
|
|
3
|
-
import type { ColumnProps } from "./types";
|
|
4
|
-
|
|
5
|
-
export const DescriptionColumn = ({ task, tree, showTree }: ColumnProps) => (
|
|
6
|
-
<Text wrap="truncate-end">{`${showTree ? renderTreePrefix(tree) : ""}${task.description}`}</Text>
|
|
7
|
-
);
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import { Text } from "ink";
|
|
2
|
-
import { formatEta } from "../format";
|
|
3
|
-
import type { ColumnProps } from "./types";
|
|
4
|
-
|
|
5
|
-
export const EtaColumn = ({ task, now }: ColumnProps) => {
|
|
6
|
-
if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") {
|
|
7
|
-
return <Text />;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const eta = formatEta(task, now);
|
|
11
|
-
const text = eta.length > 0 ? `ETA: ${eta}` : "ETA: --";
|
|
12
|
-
|
|
13
|
-
return (
|
|
14
|
-
<Text wrap="truncate-end" color="gray">
|
|
15
|
-
{text}
|
|
16
|
-
</Text>
|
|
17
|
-
);
|
|
18
|
-
};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export { AmountColumn } from "./amount-column";
|
|
2
|
-
export { BarColumn } from "./bar-column";
|
|
3
|
-
export { DescriptionColumn } from "./description-column";
|
|
4
|
-
export { ElapsedColumn } from "./elapsed-column";
|
|
5
|
-
export { EtaColumn } from "./eta-column";
|
|
6
|
-
export type { ColumnProps } from "./types";
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import type { TaskSnapshot } from "../../types";
|
|
2
|
-
import type { TaskTreeInfo } from "../types";
|
|
3
|
-
|
|
4
|
-
export interface ColumnProps {
|
|
5
|
-
readonly task: TaskSnapshot;
|
|
6
|
-
readonly tree: TaskTreeInfo;
|
|
7
|
-
readonly now: number;
|
|
8
|
-
readonly tick: number;
|
|
9
|
-
readonly isTTY: boolean;
|
|
10
|
-
readonly showTree: boolean;
|
|
11
|
-
}
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import type { TaskSnapshot } from "../types";
|
|
2
|
-
|
|
3
|
-
export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const;
|
|
4
|
-
|
|
5
|
-
export const formatDurationSeconds = (seconds: number): string => {
|
|
6
|
-
const value = Math.max(0, Math.floor(seconds));
|
|
7
|
-
if (value < 60) {
|
|
8
|
-
return `${value}s`;
|
|
9
|
-
}
|
|
10
|
-
if (value < 3600) {
|
|
11
|
-
const mins = Math.floor(value / 60);
|
|
12
|
-
const secs = value % 60;
|
|
13
|
-
return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
const hours = Math.floor(value / 3600);
|
|
17
|
-
const mins = Math.floor((value % 3600) / 60);
|
|
18
|
-
return mins > 0 ? `${hours}h ${mins}m` : `${hours}h`;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
export const formatElapsed = (task: TaskSnapshot, now: number): string => {
|
|
22
|
-
const elapsedMillis = Math.max(0, (task.completedAt ?? now) - task.startedAt);
|
|
23
|
-
return formatDurationSeconds(elapsedMillis / 1000);
|
|
24
|
-
};
|
|
25
|
-
|
|
26
|
-
export const formatEta = (task: TaskSnapshot, now: number): string => {
|
|
27
|
-
if (task.status !== "running" || task.units._tag !== "DeterminateTaskUnits") {
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const { completed, total } = task.units;
|
|
32
|
-
const remaining = total - completed;
|
|
33
|
-
if (completed <= 0 || remaining <= 0) {
|
|
34
|
-
return "";
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const elapsedMillis = Math.max(1, now - task.startedAt);
|
|
38
|
-
const etaMillis = Math.max(0, Math.floor((elapsedMillis / completed) * remaining));
|
|
39
|
-
return formatDurationSeconds(etaMillis / 1000);
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const formatAmount = (task: TaskSnapshot, tick: number): string => {
|
|
43
|
-
if (task.units._tag === "DeterminateTaskUnits") {
|
|
44
|
-
const totalText = `${task.units.total}`;
|
|
45
|
-
const completedText = `${task.units.completed}`.padStart(totalText.length, " ");
|
|
46
|
-
return `${completedText}/${totalText}`;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (task.status === "running") {
|
|
50
|
-
const frameIndex = (task.units.spinnerFrame + tick) % SPINNER_FRAMES.length;
|
|
51
|
-
return SPINNER_FRAMES[frameIndex] ?? SPINNER_FRAMES[0]!;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return task.status === "done" ? "✓" : "✗";
|
|
55
|
-
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { InkRenderer, type InkRendererService } from "./service";
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
import { formatAmount, formatElapsed, formatEta } from "./format";
|
|
2
|
-
import { renderTreePrefix } from "./tree";
|
|
3
|
-
import type { TaskRowModel } from "./types";
|
|
4
|
-
|
|
5
|
-
export const DEFAULT_BAR_WIDTH = 20;
|
|
6
|
-
const MIN_DESCRIPTION_WIDTH = 8;
|
|
7
|
-
const MIN_BAR_WIDTH = 8;
|
|
8
|
-
const MIN_ELAPSED_WIDTH = 3;
|
|
9
|
-
const MIN_AMOUNT_WIDTH = 1;
|
|
10
|
-
const BASELINE_ROW_WIDTH = 100;
|
|
11
|
-
export const MIN_DESCRIPTION_COLUMNS_FOR_TREE = 24;
|
|
12
|
-
const RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR = Array.from("59m 59s").length;
|
|
13
|
-
const RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR = Array.from("ETA: 59m 59s").length;
|
|
14
|
-
|
|
15
|
-
const textWidth = (text: string): number => Array.from(text).length;
|
|
16
|
-
|
|
17
|
-
export interface SharedColumnWidths {
|
|
18
|
-
readonly row: number;
|
|
19
|
-
readonly description: number;
|
|
20
|
-
readonly bar: number;
|
|
21
|
-
readonly amount: number;
|
|
22
|
-
readonly elapsed: number;
|
|
23
|
-
readonly eta: number;
|
|
24
|
-
readonly showTree: boolean;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const computeWidths = (
|
|
28
|
-
rows: ReadonlyArray<TaskRowModel>,
|
|
29
|
-
now: number,
|
|
30
|
-
tick: number,
|
|
31
|
-
terminalColumns?: number,
|
|
32
|
-
includeTree = true,
|
|
33
|
-
): Omit<SharedColumnWidths, "showTree"> => {
|
|
34
|
-
let hasDeterminate = false;
|
|
35
|
-
let description = MIN_DESCRIPTION_WIDTH;
|
|
36
|
-
let amount = 1;
|
|
37
|
-
let elapsed = RESERVED_ELAPSED_WIDTH_UP_TO_ONE_HOUR;
|
|
38
|
-
let eta = RESERVED_ETA_WIDTH_UP_TO_ONE_HOUR;
|
|
39
|
-
|
|
40
|
-
for (const row of rows) {
|
|
41
|
-
const { task, tree } = row;
|
|
42
|
-
const treePrefix = includeTree ? renderTreePrefix(tree) : "";
|
|
43
|
-
description = Math.max(description, textWidth(`${treePrefix}${task.description}`));
|
|
44
|
-
|
|
45
|
-
if (task.units._tag === "DeterminateTaskUnits") {
|
|
46
|
-
hasDeterminate = true;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
amount = Math.max(amount, textWidth(formatAmount(task, tick)));
|
|
50
|
-
elapsed = Math.max(elapsed, textWidth(formatElapsed(task, now)));
|
|
51
|
-
|
|
52
|
-
if (task.status === "running" && task.units._tag === "DeterminateTaskUnits") {
|
|
53
|
-
const etaValue = formatEta(task, now);
|
|
54
|
-
const etaText = `ETA: ${etaValue.length > 0 ? etaValue : "--"}`;
|
|
55
|
-
eta = Math.max(eta, textWidth(etaText));
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const bar = hasDeterminate ? DEFAULT_BAR_WIDTH : 0;
|
|
60
|
-
let widths = {
|
|
61
|
-
description,
|
|
62
|
-
bar,
|
|
63
|
-
amount,
|
|
64
|
-
elapsed,
|
|
65
|
-
eta,
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const visible = (w: typeof widths): Array<number> =>
|
|
69
|
-
[w.description, w.bar, w.amount, w.elapsed, w.eta].filter((width) => width > 0);
|
|
70
|
-
const total = (w: typeof widths): number => {
|
|
71
|
-
const cols = visible(w);
|
|
72
|
-
return cols.reduce((sum, width) => sum + width, 0) + Math.max(0, cols.length - 1);
|
|
73
|
-
};
|
|
74
|
-
|
|
75
|
-
const baselineTarget = Math.max(BASELINE_ROW_WIDTH, total(widths));
|
|
76
|
-
const target =
|
|
77
|
-
terminalColumns === undefined
|
|
78
|
-
? baselineTarget
|
|
79
|
-
: Math.max(1, Math.min(Math.max(1, Math.floor(terminalColumns)), baselineTarget));
|
|
80
|
-
|
|
81
|
-
if (total(widths) < target) {
|
|
82
|
-
widths.description += target - total(widths);
|
|
83
|
-
} else if (total(widths) > target) {
|
|
84
|
-
let overflow = total(widths) - target;
|
|
85
|
-
|
|
86
|
-
const reduceBy = (key: keyof typeof widths, min: number) => {
|
|
87
|
-
if (overflow <= 0) {
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
const current = widths[key];
|
|
91
|
-
if (current <= min) {
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
const reducible = current - min;
|
|
95
|
-
const delta = Math.min(reducible, overflow);
|
|
96
|
-
widths = { ...widths, [key]: current - delta };
|
|
97
|
-
overflow -= delta;
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
// Compress the description first, then optional columns.
|
|
101
|
-
reduceBy("description", MIN_DESCRIPTION_WIDTH);
|
|
102
|
-
reduceBy("eta", 0);
|
|
103
|
-
reduceBy("bar", MIN_BAR_WIDTH);
|
|
104
|
-
reduceBy("bar", 0);
|
|
105
|
-
reduceBy("elapsed", MIN_ELAPSED_WIDTH);
|
|
106
|
-
reduceBy("amount", MIN_AMOUNT_WIDTH);
|
|
107
|
-
reduceBy("description", 0);
|
|
108
|
-
|
|
109
|
-
if (total(widths) < target) {
|
|
110
|
-
widths.description += target - total(widths);
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const rowWidth = total(widths);
|
|
115
|
-
|
|
116
|
-
return {
|
|
117
|
-
row: rowWidth,
|
|
118
|
-
description: widths.description,
|
|
119
|
-
bar: widths.bar,
|
|
120
|
-
amount: widths.amount,
|
|
121
|
-
elapsed: widths.elapsed,
|
|
122
|
-
eta: widths.eta,
|
|
123
|
-
};
|
|
124
|
-
};
|
|
125
|
-
|
|
126
|
-
export const computeSharedColumnWidths = (
|
|
127
|
-
rows: ReadonlyArray<TaskRowModel>,
|
|
128
|
-
now: number,
|
|
129
|
-
tick: number,
|
|
130
|
-
terminalColumns?: number,
|
|
131
|
-
): SharedColumnWidths => {
|
|
132
|
-
const withTree = computeWidths(rows, now, tick, terminalColumns, true);
|
|
133
|
-
if (withTree.description >= MIN_DESCRIPTION_COLUMNS_FOR_TREE) {
|
|
134
|
-
return {
|
|
135
|
-
...withTree,
|
|
136
|
-
showTree: true,
|
|
137
|
-
};
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const withoutTree = computeWidths(rows, now, tick, terminalColumns, false);
|
|
141
|
-
return {
|
|
142
|
-
...withoutTree,
|
|
143
|
-
showTree: false,
|
|
144
|
-
};
|
|
145
|
-
};
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
import type { TaskStore } from "../types";
|
|
2
|
-
import type { OrderedTask, TaskRowModel } from "./types";
|
|
3
|
-
import { computeTreeInfo } from "./tree";
|
|
4
|
-
|
|
5
|
-
const orderedVisibleTasks = (store: TaskStore): ReadonlyArray<OrderedTask> =>
|
|
6
|
-
store.renderOrder.flatMap((row) => {
|
|
7
|
-
const snapshot = store.tasks.get(row.id);
|
|
8
|
-
if (!snapshot || (snapshot.transient && snapshot.status !== "running")) {
|
|
9
|
-
return [];
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
return [
|
|
13
|
-
{
|
|
14
|
-
snapshot,
|
|
15
|
-
depth: row.depth,
|
|
16
|
-
},
|
|
17
|
-
];
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
export const toTaskRows = (store: TaskStore): ReadonlyArray<TaskRowModel> =>
|
|
21
|
-
computeTreeInfo(orderedVisibleTasks(store)).map((entry) => ({
|
|
22
|
-
task: entry.snapshot,
|
|
23
|
-
tree: entry.tree,
|
|
24
|
-
}));
|