| 1 | import type { PinnedRuntimeProgress } from "../cloud/pinnedRuntime.ts"; |
| 2 | |
| 3 | /** A single status line below the download bar; redirected output remains plain. */ |
| 4 | export function createUpdateProgress( |
| 5 | output: Pick<NodeJS.WriteStream, "write" | "isTTY" | "columns"> = process.stderr, |
| 6 | ) { |
| 7 | const interactive = output.isTTY && process.env.TERM !== "dumb"; |
| 8 | const color = interactive && !process.env.NO_COLOR; |
| 9 | const style = (code: number, text: string) => (color ? `\x1b[${code}m${text}\x1b[0m` : text); |
| 10 | let stage: PinnedRuntimeProgress["stage"] | undefined; |
| 11 | let lastDraw = -Infinity; |
| 12 | let lineOpen = false; |
| 13 | let downloadLine = false; |
| 14 | const finish = () => { |
| 15 | if (lineOpen) output.write(downloadLine ? "\n" : "\r\x1b[2K"); |
| 16 | lineOpen = false; |
| 17 | }; |
| 18 | const status = (message: string) => { |
| 19 | finish(); |
| 20 | let line = ` ${message}`; |
| 21 | if (interactive) line = line.slice(0, Math.max(0, (output.columns || 80) - 1)); |
| 22 | output.write(interactive ? `\r\x1b[2K${style(2, line)}` : `${line}\n`); |
| 23 | lineOpen = Boolean(interactive); |
| 24 | downloadLine = false; |
| 25 | }; |
| 26 | return { |
| 27 | finish, |
| 28 | status, |
| 29 | heading(message: string, detail = "") { |
| 30 | finish(); |
| 31 | output.write(` ${style(2, message)}${detail ? ` ${style(1, detail)}` : ""}\n\n`); |
| 32 | }, |
| 33 | success(message: string) { |
| 34 | finish(); |
| 35 | output.write(` ${style(32, message)}\n\n`); |
| 36 | }, |
| 37 | report(progress: PinnedRuntimeProgress) { |
| 38 | if (progress.stage !== stage) { |
| 39 | stage = progress.stage; |
| 40 | const labels = { |
| 41 | download: "Downloading...", |
| 42 | verify: "Verifying the download...", |
| 43 | extract: "Extracting T3 Code...", |
| 44 | validate: "Checking the new executable...", |
| 45 | cached: "Using the downloaded release...", |
| 46 | }; |
| 47 | status(labels[stage]); |
| 48 | } |
| 49 | if (progress.stage !== "download" || !interactive) return; |
| 50 | const now = performance.now(); |
| 51 | if (now - lastDraw < 100 && progress.received !== progress.total) return; |
| 52 | lastDraw = now; |
| 53 | const mb = (bytes: number) => (bytes / 1024 / 1024).toFixed(1); |
| 54 | const { received, total } = progress; |
| 55 | const columns = output.columns || 80; |
| 56 | let line = |
| 57 | columns >= 32 |
| 58 | ? ` ${style(2, "Downloading")} ${mb(received)} MB` |
| 59 | : ` ${mb(received)} MB`.slice(0, Math.max(0, columns - 1)); |
| 60 | if (total !== undefined && total > 0 && columns >= 9) { |
| 61 | const percent = Math.min(100, Math.floor((received / total) * 100)); |
| 62 | const width = Math.max(1, Math.min(32, columns - 10)); |
| 63 | const filled = Math.floor((percent * width) / 100); |
| 64 | line = ` ${style(94, "■".repeat(filled))}${style(2, "·".repeat(width - filled))} ${String(percent).padStart(3)}%`; |
| 65 | if (columns >= 68) line += ` ${style(2, `${mb(received)} / ${mb(total)} MB`)}`; |
| 66 | } |
| 67 | output.write(`\r\x1b[2K${line}`); |
| 68 | lineOpen = true; |
| 69 | downloadLine = true; |
| 70 | }, |
| 71 | }; |
| 72 | } |