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,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`
2// A headless parser is available with `Parse`2// A headless parser is available with `Parse`
33
4export type Line =4export type Line =
...@@ -37,6 +37,8 @@ export async function spawn(options: SpawnOptions) {...@@ -37,6 +37,8 @@ export async function spawn(options: SpawnOptions) {
37 });37 });
38 const parser = new Parse();38 const parser = new Parse();
39 let running = true;39 let running = true;
40 let fileNode: progress.Node | null = null;
41 let fileNodeTransferNumber = 0;
4042
41 const stdoutSplitter = readline.createInterface({ input: proc.stdout });43 const stdoutSplitter = readline.createInterface({ input: proc.stdout });
42 const stderrSplitter = readline.createInterface({ input: proc.stderr });44 const stderrSplitter = readline.createInterface({ input: proc.stderr });
...@@ -51,7 +53,7 @@ export async function spawn(options: SpawnOptions) {...@@ -51,7 +53,7 @@ export async function spawn(options: SpawnOptions) {
51 node.log[result.level](result.message);53 node.log[result.level](result.message);
52 } else if (result.kind === "count") {54 } else if (result.kind === "count") {
53 if (!running) return;55 if (!running) return;
54 node.text = `${title} (${result.files} files...)`;56 node.total = result.files;
55 } else if (result.kind === "progress") {57 } else if (result.kind === "progress") {
56 if (!running) return;58 if (!running) return;
57 const {59 const {
...@@ -64,17 +66,23 @@ export async function spawn(options: SpawnOptions) {...@@ -64,17 +66,23 @@ export async function spawn(options: SpawnOptions) {
64 } = result;66 } = result;
65 node.value = transferNumber;67 node.value = transferNumber;
66 node.total = totalFiles;68 node.total = totalFiles;
67 const extras = [69 node.text = filesToCheck > 0
68 string.formatByteSize(bytesTransferred),70 ? `${title} (${filesToCheck} left)`
69 (totalFiles > filesToCheck)71 : title;
70 ? `${totalFiles - filesToCheck} unchecked`
71 : null,
72 speed,
73 ].filter(Boolean).join(", ");
74 const fileName = currentFile.length > 2072 const fileName = currentFile.length > 20
75 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`73 ? `${currentFile.slice(0, 3)}..${currentFile.slice(-15)}`
76 : currentFile;74 : 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;
78 } else result satisfies never;86 } else result satisfies never;
79 };87 };
8088
...@@ -83,6 +91,7 @@ export async function spawn(options: SpawnOptions) {...@@ -83,6 +91,7 @@ export async function spawn(options: SpawnOptions) {
8391
84 const [code, signal] = await events.once(proc, "close");92 const [code, signal] = await events.once(proc, "close");
85 running = false;93 running = false;
94 fileNode?.end();
86 if (code !== 0) {95 if (code !== 0) {
87 const fmt = code ? `code ${code}` : `signal ${signal}`;96 const fmt = code ? `code ${code}` : `signal ${signal}`;
88 const e: any = new Error(`rsync failed with ${fmt}`);97 const e: any = new Error(`rsync failed with ${fmt}`);
...@@ -119,7 +128,7 @@ export class Parse {...@@ -119,7 +128,7 @@ export class Parse {
119 totalStr,128 totalStr,
120 ] = progressMatch;129 ] = progressMatch;
121130
122 this.currentTransfer = Number(transferStr);131 if (transferStr) this.currentTransfer = Number(transferStr);
123132
124 return {133 return {
125 kind: "progress",134 kind: "progress",
...@@ -181,7 +190,6 @@ export class Parse {...@@ -181,7 +190,6 @@ export class Parse {
181import { Path } from "#sitegen/path";190import { Path } from "#sitegen/path";
182import { UNWRAP } from "@clo/lib/assert";191import { UNWRAP } from "@clo/lib/assert";
183import * as progress from "@clo/lib/progress";192import * as progress from "@clo/lib/progress";
184import * as string from "@clo/lib/string";
185import * as child_process from "node:child_process";193import * as child_process from "node:child_process";
186import events from "node:events";194import events from "node:events";
187import * as readline from "node:readline";195import * as readline from "node:readline";