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/src/runtime.ts CHANGED
@@ -4,13 +4,13 @@ import { mergeWith } from "es-toolkit/object";
4
4
  import { formatWithOptions } from "node:util";
5
5
  import type { PartialDeep } from "type-fest";
6
6
  import { makeProgressConsole } from "./console";
7
- import { BuildStage, ColorStage, FrameRenderer, ShrinkStage } from "./renderer";
7
+ import { Columns, FrameRenderer } from "./renderer";
8
8
  import { ProgressTerminal } from "./terminal";
9
- import { Theme, type ThemeService } from "./theme";
10
9
  import type {
11
10
  AddTaskOptions,
12
11
  ProgressService,
13
12
  RenderRow,
13
+ RendererConfigShape,
14
14
  TaskStore,
15
15
  UpdateTaskOptions,
16
16
  } from "./types";
@@ -130,21 +130,31 @@ const removeFromRenderOrder = (
130
130
  return next;
131
131
  };
132
132
 
133
+ const withDefaultColumns = (rendererConfig: RendererConfigShape): RendererConfigShape => ({
134
+ ...rendererConfig,
135
+ columns: rendererConfig.columns.length > 0 ? rendererConfig.columns : Columns.defaults(),
136
+ });
137
+
133
138
  const makeProgressService = Effect.gen(function* () {
134
139
  const rendererConfigOption = yield* Effect.serviceOption(RendererConfig);
135
140
  const progressBarConfigOption = yield* Effect.serviceOption(ProgressBarConfig);
136
- const rendererConfig = decodeRendererConfigSync(
137
- mergeConfig(
138
- defaultRendererConfig,
139
- Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
141
+
142
+ const rendererConfig = withDefaultColumns(
143
+ decodeRendererConfigSync(
144
+ mergeConfig(
145
+ defaultRendererConfig,
146
+ Option.isSome(rendererConfigOption) ? rendererConfigOption.value : undefined,
147
+ ),
140
148
  ),
141
149
  );
150
+
142
151
  const progressBarConfig = decodeProgressBarConfigSync(
143
152
  mergeConfig(
144
153
  defaultProgressBarConfig,
145
154
  Option.isSome(progressBarConfigOption) ? progressBarConfigOption.value : undefined,
146
155
  ),
147
156
  );
157
+
148
158
  const terminal = yield* ProgressTerminal;
149
159
  const frameRenderer = yield* FrameRenderer;
150
160
  const isTTY = yield* terminal.isTTY;
@@ -154,7 +164,6 @@ const makeProgressService = Effect.gen(function* () {
154
164
  const storeRef = yield* Ref.make<TaskStore>({
155
165
  tasks: new Map<TaskId, TaskSnapshot>(),
156
166
  renderOrder: [],
157
- themes: new Map<TaskId, ThemeService>(),
158
167
  });
159
168
  const logsRef = yield* Ref.make<ReadonlyArray<string>>([]);
160
169
  const pendingLogsRef = yield* Ref.make<ReadonlyArray<string>>([]);
@@ -163,7 +172,7 @@ const makeProgressService = Effect.gen(function* () {
163
172
  const scope = yield* Effect.scope;
164
173
 
165
174
  yield* Effect.forkIn(
166
- frameRenderer.run({
175
+ frameRenderer.run(
167
176
  storeRef,
168
177
  logsRef,
169
178
  pendingLogsRef,
@@ -172,7 +181,7 @@ const makeProgressService = Effect.gen(function* () {
172
181
  isTTY,
173
182
  rendererConfig,
174
183
  maxRetainedLogLines,
175
- }),
184
+ ),
176
185
  scope,
177
186
  );
178
187
 
@@ -184,7 +193,6 @@ const makeProgressService = Effect.gen(function* () {
184
193
  options.parentId === undefined
185
194
  ? yield* FiberRef.get(currentParentRef)
186
195
  : Option.some(options.parentId);
187
- const themeOption = yield* Effect.serviceOption(Theme);
188
196
  const taskId = TaskId(yield* Ref.updateAndGet(nextTaskIdRef, (id) => id + 1));
189
197
  const units =
190
198
  options.total === undefined || options.total <= 0
@@ -219,11 +227,7 @@ const makeProgressService = Effect.gen(function* () {
219
227
  const { index, depth } = findInsertionIndex(s.renderOrder, parentIdValue);
220
228
  const nextOrder = [...s.renderOrder];
221
229
  nextOrder.splice(index, 0, { id: taskId, depth });
222
- const nextThemes = new Map(s.themes);
223
- if (Option.isSome(themeOption)) {
224
- nextThemes.set(taskId, themeOption.value);
225
- }
226
- return { tasks: nextTasks, renderOrder: nextOrder, themes: nextThemes };
230
+ return { tasks: nextTasks, renderOrder: nextOrder };
227
231
  });
228
232
  yield* markDirty;
229
233
 
@@ -260,7 +264,7 @@ const makeProgressService = Effect.gen(function* () {
260
264
  }
261
265
  }
262
266
 
263
- return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
267
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
264
268
  }).pipe(Effect.zipRight(markDirty));
265
269
 
266
270
  const advanceTask = (taskId: TaskId, amount = 1) =>
@@ -294,7 +298,7 @@ const makeProgressService = Effect.gen(function* () {
294
298
  }),
295
299
  );
296
300
 
297
- return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
301
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
298
302
  }).pipe(Effect.zipRight(markDirty));
299
303
 
300
304
  const completeTask = (taskId: TaskId) =>
@@ -307,12 +311,9 @@ const makeProgressService = Effect.gen(function* () {
307
311
  const nextTasks = new Map(store.tasks);
308
312
  if (snapshot.transient) {
309
313
  nextTasks.delete(taskId);
310
- const nextThemes = new Map(store.themes);
311
- nextThemes.delete(taskId);
312
314
  return {
313
315
  tasks: nextTasks,
314
316
  renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
315
- themes: nextThemes,
316
317
  };
317
318
  }
318
319
 
@@ -336,7 +337,7 @@ const makeProgressService = Effect.gen(function* () {
336
337
  completedAt: now,
337
338
  }),
338
339
  );
339
- return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
340
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
340
341
  });
341
342
  yield* markDirty;
342
343
  });
@@ -351,12 +352,9 @@ const makeProgressService = Effect.gen(function* () {
351
352
  const nextTasks = new Map(store.tasks);
352
353
  if (snapshot.transient) {
353
354
  nextTasks.delete(taskId);
354
- const nextThemes = new Map(store.themes);
355
- nextThemes.delete(taskId);
356
355
  return {
357
356
  tasks: nextTasks,
358
357
  renderOrder: removeFromRenderOrder(store.renderOrder, taskId),
359
- themes: nextThemes,
360
358
  };
361
359
  }
362
360
 
@@ -374,7 +372,7 @@ const makeProgressService = Effect.gen(function* () {
374
372
  completedAt: now,
375
373
  }),
376
374
  );
377
- return { tasks: nextTasks, renderOrder: store.renderOrder, themes: store.themes };
375
+ return { tasks: nextTasks, renderOrder: store.renderOrder };
378
376
  });
379
377
  yield* markDirty;
380
378
  });
@@ -385,7 +383,6 @@ const makeProgressService = Effect.gen(function* () {
385
383
  return;
386
384
  }
387
385
 
388
- // TODO: Might wanna replace this or make it configurable. Look for other options.
389
386
  const message = formatWithOptions(
390
387
  {
391
388
  colors: isTTY,
@@ -486,29 +483,13 @@ export class Progress extends Context.Tag("stromseng.dev/effective-progress/Prog
486
483
  >() {
487
484
  static readonly Default = Layer.unwrapEffect(
488
485
  Effect.gen(function* () {
489
- const themeOption = yield* Effect.serviceOption(Theme);
490
486
  const terminalOption = yield* Effect.serviceOption(ProgressTerminal);
491
- const buildStageOption = yield* Effect.serviceOption(BuildStage);
492
- const shrinkStageOption = yield* Effect.serviceOption(ShrinkStage);
493
- const colorStageOption = yield* Effect.serviceOption(ColorStage);
494
487
  const frameRendererOption = yield* Effect.serviceOption(FrameRenderer);
495
488
  let layer: Layer.Layer<Progress, never, any> = Layer.scoped(Progress, makeProgressService);
496
489
 
497
490
  if (Option.isNone(frameRendererOption)) {
498
491
  layer = layer.pipe(Layer.provide(FrameRenderer.Default));
499
492
  }
500
- if (Option.isNone(colorStageOption)) {
501
- layer = layer.pipe(Layer.provide(ColorStage.Default));
502
- }
503
- if (Option.isNone(shrinkStageOption)) {
504
- layer = layer.pipe(Layer.provide(ShrinkStage.Default));
505
- }
506
- if (Option.isNone(buildStageOption)) {
507
- layer = layer.pipe(Layer.provide(BuildStage.Default));
508
- }
509
- if (Option.isNone(themeOption)) {
510
- layer = layer.pipe(Layer.provide(Theme.Default));
511
- }
512
493
  if (Option.isNone(terminalOption)) {
513
494
  layer = layer.pipe(Layer.provide(ProgressTerminal.Default));
514
495
  }
package/src/types.ts CHANGED
@@ -1,17 +1,36 @@
1
1
  import { Brand, Context, Effect, Option, Schema } from "effect";
2
2
  import type { PartialDeep } from "type-fest";
3
- import type { ThemeService } from "./theme";
3
+ import type { ProgressColumn } from "./renderer";
4
4
 
5
5
  export const RendererConfigSchema = Schema.Struct({
6
6
  disableUserInput: Schema.Boolean,
7
7
  renderIntervalMillis: Schema.Number,
8
8
  maxLogLines: Schema.optional(Schema.Number),
9
9
  nonTtyUpdateStep: Schema.Number,
10
- determinateTaskLayout: Schema.Literal("single-line", "two-lines"),
11
- maxTaskWidth: Schema.optional(Schema.Number),
10
+ width: Schema.Union(Schema.Number, Schema.Literal("fullwidth")),
11
+ columnGap: Schema.Number,
12
+ columns: Schema.Array(Schema.Unknown),
12
13
  });
13
- export type RendererConfigShape = typeof RendererConfigSchema.Type;
14
- export const decodeRendererConfigSync = Schema.decodeUnknownSync(RendererConfigSchema);
14
+ export type RendererConfigShape = {
15
+ readonly disableUserInput: boolean;
16
+ readonly renderIntervalMillis: number;
17
+ readonly maxLogLines?: number;
18
+ readonly nonTtyUpdateStep: number;
19
+ readonly width: number | "fullwidth";
20
+ readonly columnGap: number;
21
+ readonly columns: ReadonlyArray<ProgressColumn | string>;
22
+ };
23
+ const decodeRendererConfigSchemaSync = Schema.decodeUnknownSync(RendererConfigSchema);
24
+ export const decodeRendererConfigSync = (input: unknown): RendererConfigShape => {
25
+ if (typeof input === "object" && input !== null && "determinateTaskLayout" in input) {
26
+ throw new Error("determinateTaskLayout has been removed. Use columns instead.");
27
+ }
28
+ if (typeof input === "object" && input !== null && "maxTaskWidth" in input) {
29
+ throw new Error("maxTaskWidth has been removed. Use width on RendererConfig instead.");
30
+ }
31
+
32
+ return decodeRendererConfigSchemaSync(input) as RendererConfigShape;
33
+ };
15
34
 
16
35
  export const ProgressBarConfigSchema = Schema.Struct({
17
36
  spinnerFrames: Schema.NonEmptyArray(Schema.String),
@@ -29,7 +48,9 @@ export const defaultRendererConfig: RendererConfigShape = {
29
48
  renderIntervalMillis: 100, // 10 FPS
30
49
  maxLogLines: 0,
31
50
  nonTtyUpdateStep: 5,
32
- determinateTaskLayout: "single-line",
51
+ width: 120,
52
+ columnGap: 1,
53
+ columns: [],
33
54
  };
34
55
 
35
56
  export const defaultProgressBarConfig: ProgressBarConfigShape = {
@@ -115,7 +136,6 @@ export interface RenderRow {
115
136
  export interface TaskStore {
116
137
  readonly tasks: Map<TaskId, TaskSnapshot>;
117
138
  readonly renderOrder: ReadonlyArray<RenderRow>;
118
- readonly themes: Map<TaskId, ThemeService>;
119
139
  }
120
140
 
121
141
  export interface ProgressService {
package/src/theme.ts DELETED
@@ -1,87 +0,0 @@
1
- import chalk, { type ChalkInstance } from "chalk";
2
- import { Context, Effect, Layer } from "effect";
3
-
4
- export type ThemeRole =
5
- | "plain"
6
- | "barFill"
7
- | "barEmpty"
8
- | "barBracket"
9
- | "spinner"
10
- | "statusDone"
11
- | "statusFailed"
12
- | "text"
13
- | "units"
14
- | "eta"
15
- | "elapsed"
16
- | "treeConnector";
17
-
18
- export type ThemeStyle = (text: string) => string;
19
-
20
- export interface ThemeService {
21
- readonly styles: Readonly<Record<ThemeRole, ThemeStyle>>;
22
- readonly depthPalette?: (depth: number, role: ThemeRole) => ThemeStyle | undefined;
23
- }
24
-
25
- export class Theme extends Context.Tag("stromseng.dev/effective-progress/Theme")<
26
- Theme,
27
- ThemeService
28
- >() {
29
- static readonly Default = Layer.succeed(
30
- Theme,
31
- Theme.of({
32
- styles: {
33
- plain: (text) => text,
34
- barFill: chalk.blue,
35
- barEmpty: chalk.white.dim,
36
- barBracket: chalk.white.dim,
37
- spinner: chalk.yellow,
38
- statusDone: chalk.green,
39
- statusFailed: chalk.red,
40
- text: (text) => text,
41
- units: chalk.whiteBright,
42
- eta: chalk.gray,
43
- elapsed: chalk.gray,
44
- treeConnector: chalk.gray,
45
- },
46
- }),
47
- );
48
-
49
- static readonly Rainbow = Layer.unwrapEffect(
50
- Effect.gen(function* () {
51
- let colorNumber = 17;
52
- yield* Effect.fork(
53
- Effect.gen(function* () {
54
- while (true) {
55
- colorNumber = (colorNumber + 1) % 256;
56
- yield* Effect.sleep("200 millis");
57
- }
58
- }),
59
- );
60
-
61
- const dynamic =
62
- (instance: ChalkInstance): ThemeStyle =>
63
- (text) =>
64
- instance.ansi256(colorNumber)(text);
65
-
66
- return Layer.succeed(
67
- Theme,
68
- Theme.of({
69
- styles: {
70
- plain: (text) => text,
71
- barFill: dynamic(chalk),
72
- barEmpty: dynamic(chalk.dim),
73
- barBracket: dynamic(chalk),
74
- spinner: dynamic(chalk),
75
- statusDone: dynamic(chalk.bold),
76
- statusFailed: dynamic(chalk.bold),
77
- text: dynamic(chalk),
78
- units: dynamic(chalk),
79
- eta: dynamic(chalk),
80
- elapsed: dynamic(chalk),
81
- treeConnector: dynamic(chalk),
82
- },
83
- }),
84
- );
85
- }),
86
- );
87
- }