dictum-cli 0.1.0

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.
@@ -0,0 +1,91 @@
1
+ /**
2
+ * ui/status.ts — render a live status line from pipeline stage events.
3
+ *
4
+ * Stages map to glyphs: ● recording / ◌ transcribing / ✦ polishing, finishing
5
+ * with ✓ <done> <timing>. Output goes to a stream (stderr by default) so stdout
6
+ * stays clean for pipes. On a TTY the line is updated in place; otherwise only
7
+ * completed stages are printed (no spinner spam in logs/pipes).
8
+ *
9
+ * Driven by the StageEvent stream already emitted by core/pipeline.ts.
10
+ */
11
+
12
+ import type { PipelineStage, StageEvent } from "../core/types.ts"
13
+
14
+ const ACTIVE: Partial<Record<PipelineStage, { glyph: string; label: string }>> = {
15
+ recording: { glyph: "●", label: "recording" },
16
+ transcribing: { glyph: "◌", label: "transcribing" },
17
+ polishing: { glyph: "✦", label: "polishing" },
18
+ emitting: { glyph: "✦", label: "emitting" },
19
+ }
20
+
21
+ const DONE_LABEL: Partial<Record<PipelineStage, string>> = {
22
+ recording: "recorded",
23
+ transcribing: "transcribed",
24
+ polishing: "polished",
25
+ }
26
+
27
+ const DEFAULT_EMIT_LABEL = "copied"
28
+ const CHECK = "✓"
29
+ const CLEAR_LINE = "\r\x1b[2K"
30
+
31
+ /** Format a millisecond duration as a short human string ("340ms" / "1.2s"). */
32
+ export function formatDuration(ms: number): string {
33
+ if (!Number.isFinite(ms) || ms < 0) return ""
34
+ if (ms < 1000) return `${Math.round(ms)}ms`
35
+ return `${(ms / 1000).toFixed(1)}s`
36
+ }
37
+
38
+ /**
39
+ * Render a single status line for an event, or null when there's nothing to
40
+ * show. `emitLabel` names the completed emit stage (e.g. "copied" for the
41
+ * clipboard, "written" for stdout).
42
+ */
43
+ export function renderStageLine(
44
+ event: StageEvent,
45
+ emitLabel: string = DEFAULT_EMIT_LABEL,
46
+ ): string | null {
47
+ if (event.stage === "done") return null
48
+ if (event.durationMs === undefined) {
49
+ const active = ACTIVE[event.stage]
50
+ return active ? `${active.glyph} ${active.label}…` : null
51
+ }
52
+ const label = event.stage === "emitting" ? emitLabel : DONE_LABEL[event.stage]
53
+ if (!label) return null
54
+ const timing = formatDuration(event.durationMs)
55
+ return timing ? `${CHECK} ${label} ${timing}` : `${CHECK} ${label}`
56
+ }
57
+
58
+ export type StatusStream = { write(s: string): unknown; isTTY?: boolean }
59
+
60
+ /** Consumes pipeline StageEvents and renders a status line. */
61
+ export class StatusReporter {
62
+ private readonly stream: StatusStream
63
+ private readonly tty: boolean
64
+ private readonly emitLabel: string
65
+
66
+ constructor(
67
+ stream: StatusStream = process.stderr,
68
+ opts: { tty?: boolean; emitLabel?: string } = {},
69
+ ) {
70
+ this.stream = stream
71
+ this.tty = opts.tty ?? Boolean(stream.isTTY)
72
+ this.emitLabel = opts.emitLabel ?? DEFAULT_EMIT_LABEL
73
+ }
74
+
75
+ /** Bound so it can be passed directly as the pipeline's `onStage` callback. */
76
+ readonly onStage = (event: StageEvent): void => {
77
+ if (event.stage === "done") {
78
+ if (this.tty) this.stream.write("\n")
79
+ return
80
+ }
81
+ const line = renderStageLine(event, this.emitLabel)
82
+ if (!line) return
83
+ const active = event.durationMs === undefined
84
+ if (this.tty) {
85
+ this.stream.write(`${CLEAR_LINE}${line}${active ? "" : "\n"}`)
86
+ } else if (!active) {
87
+ // Non-TTY: only print completed stages, one per line.
88
+ this.stream.write(`${line}\n`)
89
+ }
90
+ }
91
+ }