authorgravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-05-17 21:27:41-07:00
committergravatar for git@paperclover.netclover caruso <git@paperclover.net> 2026-05-28 02:30:18-07:00
logcd50eaf690b7e79b30b38b9c27f2c686ea51f4f2
tree3cb2296cd926792e8b3a6657e33a28684d7edff2
parentc9d839e19130931ec7a40d375ed694d3d3f4042f
signature Signed by SSH key SHA256:xbd+BjjhyBfwk7GVoURf9Yx0gzDerHbvYv7SddNWmAs

feat: nicer rsync progress indicator


1 files changed, 20 insertions(+), 12 deletions(-)

src/file-viewer/rsync.ts+20-12
......@@ -1,4 +1,4 @@
1// Utilities for spawning rsync and consuming its output as a `Progress`
1// Utilities for spawning rsync and consuming its output as a `progress.Node`
22// A headless parser is available with `Parse`
33
44export type Line =
......@@ -37,6 +37,8 @@ export async function spawn(options: SpawnOptions) {
3737 });
3838 const parser = new Parse();
3939 let running = true;
40 let fileNode: progress.Node | null = null;
41 let fileNodeTransferNumber = 0;
4042
4143 const stdoutSplitter = readline.createInterface({ input: proc.stdout });
4244 const stderrSplitter = readline.createInterface({ input: proc.stderr });
......@@ -51,7 +53,7 @@ export async function spawn(options: SpawnOptions) {
5153 node.log[result.level](result.message);
5254 } else if (result.kind === "count") {
5355 if (!running) return;
54 node.text = `${title} (${result.files} files...)`;
56 node.total = result.files;
5557 } else if (result.kind === "progress") {
5658 if (!running) return;
5759 const {
......@@ -64,17 +66,23 @@ export async function spawn(options: SpawnOptions) {
6466 } = result;
6567 node.value = transferNumber;
6668 node.total = totalFiles;
67 const extras = [
68 string.formatByteSize(bytesTransferred),
69 (totalFiles > filesToCheck)
70 ? `${totalFiles - filesToCheck} unchecked`
71 : null,
72 speed,
73 ].filter(Boolean).join(", ");
69 node.text = filesToCheck > 0
70 ? `${title} (${filesToCheck} left)`
71 : title;
7472 const fileName = currentFile.length > 20
7573 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`
7674 : currentFile;
77 node.text = `${title} - ${fileName}${extras.length > 0 ? ` (${extras})` : ""}`;
75 if (!fileNode || fileNodeTransferNumber !== transferNumber) {
76 fileNode?.end();
77 fileNode = node.start(fileName, { units: "bytes" });
78 fileNodeTransferNumber = transferNumber;
79 }
80 const estimatedTotal = result.percentage > 0
81 ? Math.ceil(bytesTransferred / (result.percentage / 100))
82 : 0;
83 fileNode.text = speed ? `${fileName} (${speed})` : fileName;
84 fileNode.value = bytesTransferred;
85 fileNode.total = estimatedTotal;
7886 } else result satisfies never;
7987 };
8088
......@@ -83,6 +91,7 @@ export async function spawn(options: SpawnOptions) {
8391
8492 const [code, signal] = await events.once(proc, "close");
8593 running = false;
94 fileNode?.end();
8695 if (code !== 0) {
8796 const fmt = code ? `code ${code}` : `signal ${signal}`;
8897 const e: any = new Error(`rsync failed with ${fmt}`);
......@@ -119,7 +128,7 @@ export class Parse {
119128 totalStr,
120129 ] = progressMatch;
121130
122 this.currentTransfer = Number(transferStr);
131 if (transferStr) this.currentTransfer = Number(transferStr);
123132
124133 return {
125134 kind: "progress",
......@@ -181,7 +190,6 @@ export class Parse {
181190import { Path } from "#sitegen/path";
182191import { UNWRAP } from "@clo/lib/assert";
183192import * as progress from "@clo/lib/progress";
184import * as string from "@clo/lib/string";
185193import * as child_process from "node:child_process";
186194import events from "node:events";
187195import * as readline from "node:readline";