@bobfrankston/brother-label 1.0.14 → 1.1.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/cli.ts CHANGED
@@ -1,344 +1,301 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Brother Label Printer CLI
4
- * Thin wrapper around the API
5
- */
6
-
7
- import * as fs from "fs";
8
- import * as path from "path";
9
- import {
10
- print,
11
- render,
12
- renderSegments,
13
- printSegments,
14
- getConfig,
15
- setConfig,
16
- getConfigPath,
17
- listPrinters,
18
- TapeSize,
19
- PrintOptions,
20
- Segment,
21
- } from "./api.js";
22
-
23
- const VERSION = "1.0.13";
24
- const VALID_TAPES: TapeSize[] = [6, 9, 12, 18, 24];
25
-
26
- function parseTape(value: string): TapeSize {
27
- const num = parseInt(value.replace("mm", ""), 10) as TapeSize;
28
- if (!VALID_TAPES.includes(num)) {
29
- throw new Error(`Invalid tape size: ${value}. Valid: 6, 9, 12, 18, 24`);
30
- }
31
- return num;
32
- }
33
-
34
- // Detect content type from input string
35
- function buildPrintOptions(input: string, opts: { tape?: string; printer?: string; text?: boolean; html?: boolean; image?: boolean; aspect?: string; height?: string }): PrintOptions {
36
- const options: PrintOptions = {};
37
-
38
- // Parse tape option
39
- if (opts.tape) {
40
- options.tape = parseTape(opts.tape);
41
- }
42
- if (opts.printer) {
43
- options.printer = opts.printer;
44
- }
45
- if (opts.aspect) {
46
- options.aspect = opts.aspect;
47
- }
48
- if (opts.height) {
49
- options.textHeight = opts.height;
50
- }
51
-
52
- // Explicit type flags override auto-detection
53
- if (opts.text) {
54
- options.text = input;
55
- return options;
56
- }
57
- if (opts.html) {
58
- options.htmlPath = path.resolve(input);
59
- return options;
60
- }
61
- if (opts.image) {
62
- options.imagePath = path.resolve(input);
63
- return options;
64
- }
65
-
66
- // Auto-detect content type
67
- const lower = input.toLowerCase();
68
- if (lower.endsWith(".html") || lower.endsWith(".htm")) {
69
- options.htmlPath = path.resolve(input);
70
- } else if (lower.endsWith(".txt")) {
71
- options.textFile = path.resolve(input);
72
- } else if (lower.endsWith(".png") || lower.endsWith(".jpg") || lower.endsWith(".jpeg") || lower.endsWith(".bmp") || lower.endsWith(".gif")) {
73
- options.imagePath = path.resolve(input);
74
- } else if (fs.existsSync(input)) {
75
- // Exists but unknown extension - try to detect
76
- const ext = path.extname(lower);
77
- if (ext === ".html" || ext === ".htm") {
78
- options.htmlPath = path.resolve(input);
79
- } else {
80
- // Assume text file
81
- options.textFile = path.resolve(input);
82
- }
83
- } else {
84
- // Treat as literal text
85
- options.text = input;
86
- }
87
-
88
- return options;
89
- }
90
-
91
- interface ParsedArgs {
92
- command: string | null;
93
- opts: Record<string, string | boolean>;
94
- segments: Segment[];
95
- input: string | null;
96
- }
97
-
98
- // Valued options: flag name(s) → canonical key
99
- const VALUED_OPTIONS: [string[], string][] = [
100
- [["-tape"], "tape"],
101
- [["-p", "-printer"], "printer"],
102
- [["-o", "-output"], "output"],
103
- [["-a", "-aspect"], "aspect"],
104
- [["-H", "-height"], "height"],
105
- [["-s", "-sp", "-space"], "space"],
106
- ];
107
-
108
- // Boolean flags: flag name(s) → canonical key
109
- const BOOLEAN_FLAGS: [string[], string][] = [
110
- [["-w", "-html"], "html"],
111
- [["-i", "-image"], "image"],
112
- [["-t"], "text"],
113
- [["-help", "-h"], "help"],
114
- [["-version", "-v"], "version"],
115
- ];
116
-
117
- function parseArgs(argv: string[]): ParsedArgs {
118
- const args = argv.slice(2).map(a => a.startsWith("--") ? a.slice(1) : a);
119
- const command: string | null = null;
120
- const opts: Record<string, string | boolean> = {};
121
- const segments: Segment[] = [];
122
- let input: string | null = null;
123
- let i = 0;
124
-
125
- while (i < args.length) {
126
- const arg = args[i];
127
-
128
- // Segment flags: -text <value>, -qr <value>
129
- if ((arg === "-text" || arg === "-qr") && i + 1 < args.length) {
130
- segments.push({ type: arg === "-text" ? "text" : "qr", value: args[i + 1] });
131
- i += 2;
132
- continue;
133
- }
134
-
135
- // Valued options
136
- let matched = false;
137
- for (const [names, key] of VALUED_OPTIONS) {
138
- if (names.includes(arg) && i + 1 < args.length) {
139
- opts[key] = args[i + 1];
140
- i += 2;
141
- matched = true;
142
- break;
143
- }
144
- }
145
- if (matched) continue;
146
-
147
- // Boolean flags
148
- for (const [names, key] of BOOLEAN_FLAGS) {
149
- if (names.includes(arg)) {
150
- opts[key] = true;
151
- i++;
152
- matched = true;
153
- break;
154
- }
155
- }
156
- if (matched) continue;
157
-
158
- // Subcommands
159
- if ((arg === "list" || arg === "config") && command === null && input === null) {
160
- return { ...parseRest(args, i), command: arg };
161
- }
162
-
163
- // Bare string → positional input (first one wins)
164
- if (input === null) {
165
- input = arg;
166
- }
167
- i++;
168
- }
169
-
170
- return { command, opts, segments, input };
171
- }
172
-
173
- // After recognizing a subcommand, parse the rest for that subcommand's options
174
- function parseRest(args: string[], startIndex: number): ParsedArgs {
175
- const opts: Record<string, string | boolean> = {};
176
- const segments: Segment[] = [];
177
- let input: string | null = null;
178
- let i = startIndex + 1;
179
-
180
- while (i < args.length) {
181
- const arg = args[i];
182
- let matched = false;
183
- for (const [names, key] of VALUED_OPTIONS) {
184
- if (names.includes(arg) && i + 1 < args.length) {
185
- opts[key] = args[i + 1];
186
- i += 2;
187
- matched = true;
188
- break;
189
- }
190
- }
191
- if (matched) continue;
192
-
193
- for (const [names, key] of BOOLEAN_FLAGS) {
194
- if (names.includes(arg)) {
195
- opts[key] = true;
196
- i++;
197
- matched = true;
198
- break;
199
- }
200
- }
201
- if (matched) continue;
202
-
203
- if (input === null) input = arg;
204
- i++;
205
- }
206
-
207
- return { command: null, opts, segments, input };
208
- }
209
-
210
- function showHelp(): void {
211
- console.log(`Brother Label Printer CLI v${VERSION}
212
-
213
- Usage:
214
- brother-print [options] <input> Print text, file, or image
215
- brother-print -text <v> -qr <v> ... Print ordered segments
216
- brother-print list List available printers
217
- brother-print config Show/set configuration
218
-
219
- Options:
220
- -tape <size> Tape size: 6, 9, 12, 18, 24 (mm)
221
- -p, -printer <n> Printer name
222
- -o, -output <file> Save to file instead of printing (png, jpg, bmp)
223
- -a, -aspect <r> Aspect ratio width:height for HTML (e.g., 3.5:2)
224
- -H, -height <size> Text height: 12mm, .5in, or 50% (of tape height)
225
- -s, -space <size> Space between segments: 12px, 1mm, .2in
226
- -t, -text Force input as literal text (or -text <v> for segments)
227
- -qr <data> QR code segment
228
- -w, -html Force input as HTML file path
229
- -i, -image Force input as image file path
230
- -help Show this help
231
- -version Show version`);
232
- }
233
-
234
- // --- Main ---
235
-
236
- async function main(): Promise<void> {
237
- const parsed = parseArgs(process.argv);
238
-
239
- if (parsed.opts.help) {
240
- showHelp();
241
- return;
242
- }
243
- if (parsed.opts.version) {
244
- console.log(VERSION);
245
- return;
246
- }
247
-
248
- // Subcommands
249
- if (parsed.command === "list") {
250
- try {
251
- const printers = await listPrinters();
252
- if (printers.length === 0) {
253
- console.log("No Brother printers found");
254
- } else {
255
- console.log("Brother printers:");
256
- printers.forEach(p => console.log(` ${p.name}`));
257
- }
258
- } catch (err) {
259
- console.error(`Error: ${(err as Error).message}`);
260
- process.exit(1);
261
- }
262
- return;
263
- }
264
-
265
- if (parsed.command === "config") {
266
- try {
267
- if (!parsed.opts.tape && !parsed.opts.printer) {
268
- const config = getConfig();
269
- console.log("Configuration:");
270
- console.log(` File: ${getConfigPath()}`);
271
- console.log(` Default tape: ${config.defaultTape ? config.defaultTape + "mm" : "(not set)"}`);
272
- console.log(` Default printer: ${config.defaultPrinter ?? "(not set)"}`);
273
- console.log("\nValid tape sizes: 6, 9, 12, 18, 24");
274
- return;
275
- }
276
- if (parsed.opts.tape) {
277
- const tape = parseTape(parsed.opts.tape as string);
278
- setConfig({ defaultTape: tape });
279
- console.log(`Default tape set to: ${tape}mm`);
280
- }
281
- if (parsed.opts.printer) {
282
- setConfig({ defaultPrinter: parsed.opts.printer as string });
283
- console.log(`Default printer set to: ${parsed.opts.printer}`);
284
- }
285
- } catch (err) {
286
- console.error(`Error: ${(err as Error).message}`);
287
- process.exit(1);
288
- }
289
- return;
290
- }
291
-
292
- // Multi-segment mode
293
- if (parsed.segments.length > 0) {
294
- try {
295
- const tape = parsed.opts.tape ? parseTape(parsed.opts.tape as string) : undefined;
296
- if (parsed.opts.output) {
297
- const buffer = await renderSegments(parsed.segments, tape, parsed.opts.height as string | undefined, parsed.opts.space as string | undefined);
298
- fs.writeFileSync(parsed.opts.output as string, buffer);
299
- console.log(`Saved to ${parsed.opts.output}`);
300
- } else {
301
- await printSegments(parsed.segments, { tape, printer: parsed.opts.printer as string | undefined, textHeight: parsed.opts.height as string | undefined, space: parsed.opts.space as string | undefined });
302
- const effectiveTape = tape ?? getConfig().defaultTape ?? 24;
303
- console.log(`Printed ${parsed.segments.length} segments on ${effectiveTape}mm tape`);
304
- }
305
- } catch (err) {
306
- console.error(`Error: ${(err as Error).message}`);
307
- process.exit(1);
308
- }
309
- return;
310
- }
311
-
312
- // Single input mode
313
- if (!parsed.input) {
314
- showHelp();
315
- return;
316
- }
317
-
318
- try {
319
- const options = buildPrintOptions(parsed.input, {
320
- tape: parsed.opts.tape as string | undefined,
321
- printer: parsed.opts.printer as string | undefined,
322
- text: parsed.opts.text as boolean | undefined,
323
- html: parsed.opts.html as boolean | undefined,
324
- image: parsed.opts.image as boolean | undefined,
325
- aspect: parsed.opts.aspect as string | undefined,
326
- height: parsed.opts.height as string | undefined,
327
- });
328
-
329
- if (parsed.opts.output) {
330
- const buffer = await render(options);
331
- fs.writeFileSync(parsed.opts.output as string, buffer);
332
- console.log(`Saved to ${parsed.opts.output}`);
333
- } else {
334
- await print(options);
335
- const tape = options.tape ?? getConfig().defaultTape ?? 24;
336
- console.log(`Printed on ${tape}mm tape`);
337
- }
338
- } catch (err) {
339
- console.error(`Error: ${(err as Error).message}`);
340
- process.exit(1);
341
- }
342
- }
343
-
344
- main();
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Brother Label Printer CLI
4
+ * Thin wrapper around api.ts using shared CLI primitives from label-core.
5
+ */
6
+
7
+ import * as fs from "fs";
8
+ import * as path from "path";
9
+ import {
10
+ preprocessSingleQuotes,
11
+ parseArgs,
12
+ } from "@bobfrankston/label-core";
13
+ import type {
14
+ PrintOptions as CorePrintOptions,
15
+ PrinterStatus,
16
+ Segment,
17
+ ValuedOptionSpec,
18
+ BooleanFlagSpec,
19
+ } from "@bobfrankston/label-core";
20
+ import {
21
+ print,
22
+ render,
23
+ renderSegments,
24
+ printSegments,
25
+ getConfig,
26
+ setConfig,
27
+ getConfigPath,
28
+ listPrinters,
29
+ detectTapeSize,
30
+ brotherPrinter,
31
+ } from "./api.js";
32
+ import type { TapeSize, PrintOptions, SegmentOptions } from "./api.js";
33
+
34
+ const VERSION = "1.1.0";
35
+ const VALID_TAPES: TapeSize[] = [6, 9, 12, 18, 24];
36
+
37
+ function parseTape(value: string): TapeSize {
38
+ const num = parseInt(value.replace("mm", ""), 10) as TapeSize;
39
+ if (!VALID_TAPES.includes(num)) {
40
+ throw new Error(`Invalid tape size: ${value}. Valid: ${VALID_TAPES.join(", ")}`);
41
+ }
42
+ return num;
43
+ }
44
+
45
+ const VALUED: ValuedOptionSpec[] = [
46
+ { names: ["-tape"], key: "tape" },
47
+ { names: ["-p", "-printer"], key: "printer" },
48
+ { names: ["-o", "-output"], key: "output" },
49
+ { names: ["-a", "-aspect"], key: "aspect" },
50
+ { names: ["-H", "-height"], key: "height" },
51
+ { names: ["-s", "-space"], key: "space" },
52
+ { names: ["-timeout"], key: "timeout" },
53
+ { names: ["-interval"], key: "interval" },
54
+ ];
55
+
56
+ const BOOLEAN: BooleanFlagSpec[] = [
57
+ { names: ["-w", "-html"], key: "html" },
58
+ { names: ["-i", "-image"], key: "image" },
59
+ { names: ["-t"], key: "text" },
60
+ { names: ["-c", "-clip"], key: "clip" },
61
+ { names: ["-no-wait", "-nowait"], key: "nowait" },
62
+ { names: ["-help", "-h", "-?"], key: "help" },
63
+ { names: ["-version", "-v"], key: "version" },
64
+ ];
65
+
66
+ const SUBCOMMANDS = ["list", "status", "config"];
67
+
68
+ function showHelp(): void {
69
+ console.log(`Brother Label Printer CLI v${VERSION}
70
+
71
+ Usage:
72
+ brother-print [options] <text> Print text
73
+ brother-print [options] <file> Print html/txt/image file (auto-detected)
74
+ brother-print -clip [options] Print clipboard contents (image or text)
75
+ brother-print -text <v> -qr <v> ... Print ordered text/qr segments
76
+ brother-print list List Brother printers and status
77
+ brother-print status [-p <name>] Show printer status
78
+ brother-print config Show / set defaults
79
+
80
+ Tape size is auto-detected from the printer. Use -tape to override.
81
+ Single quotes can wrap arguments: '7"' 'line1\\nline2'
82
+
83
+ Options:
84
+ -tape <size> Tape size: 6, 9, 12, 18, 24 (mm) [auto-detected]
85
+ -p, -printer <name> Printer queue name
86
+ -o, -output <file> Save PNG to file instead of printing
87
+ -a, -aspect <r> Aspect ratio width:height for HTML (e.g. 3.5:2)
88
+ -H, -height <size> Text height: 12mm, .5in, or 50% (of tape height)
89
+ -s, -space <size> Space between segments: 12px, 1mm, .2in
90
+ -t, -text Force input as literal text (-text <v> for segments)
91
+ -qr <data> QR code segment
92
+ -w, -html Force input as HTML file path
93
+ -i, -image Force input as image file path
94
+ -c, -clip Read content from clipboard (image preferred, then text)
95
+ -no-wait Fail immediately if printer is offline (default: wait)
96
+ -timeout <secs> Max wait time when printer is offline
97
+ -interval <secs> Polling interval while waiting (default 2)
98
+ -help Show this help
99
+ -version Show version`);
100
+ }
101
+
102
+ async function main(): Promise<void> {
103
+ const argv = preprocessSingleQuotes(process.argv.slice(2));
104
+ let parsed;
105
+ try {
106
+ parsed = parseArgs(argv, VALUED, BOOLEAN, SUBCOMMANDS);
107
+ } catch (e: any) {
108
+ console.error(`Error: ${e.message}`);
109
+ process.exit(1);
110
+ }
111
+
112
+ if (parsed.unknown.length > 0) {
113
+ console.error(`Unknown option(s): ${parsed.unknown.join(", ")}`);
114
+ console.error(`Run "brother-print -help" for usage.`);
115
+ process.exit(1);
116
+ }
117
+
118
+ if (parsed.opts.help) { showHelp(); return; }
119
+ if (parsed.opts.version) { console.log(VERSION); return; }
120
+
121
+ try {
122
+ switch (parsed.command) {
123
+ case "list": await cmdList(); return;
124
+ case "status": await cmdStatus(parsed.opts); return;
125
+ case "config": await cmdConfig(parsed.opts); return;
126
+ }
127
+ await cmdPrint(parsed.opts, parsed.segments, parsed.inputs);
128
+ } catch (e: any) {
129
+ console.error(`Error: ${e.message}`);
130
+ process.exit(1);
131
+ }
132
+ }
133
+
134
+ async function cmdList(): Promise<void> {
135
+ const printers = await listPrinters();
136
+ if (printers.length === 0) {
137
+ console.log("No Brother printers found.");
138
+ return;
139
+ }
140
+ console.log(`Brother printers (${printers.length}):`);
141
+ for (const p of printers) {
142
+ let statusStr = "?";
143
+ try {
144
+ const s = await brotherPrinter.getStatus(p.name);
145
+ statusStr = s.online ? "online" : `offline (${s.statusText}${s.error ? ", " + s.error : ""})`;
146
+ } catch (e: any) {
147
+ statusStr = `status error: ${e.message}`;
148
+ }
149
+ console.log(` ${p.name} — ${statusStr}`);
150
+ }
151
+ }
152
+
153
+ async function cmdStatus(opts: Record<string, string | boolean>): Promise<void> {
154
+ const name = (opts.printer as string) || undefined;
155
+ const s = await brotherPrinter.getStatus(name);
156
+ console.log(`Printer: ${s.name}`);
157
+ console.log(`Online: ${s.online ? "yes" : "no"}`);
158
+ console.log(`Status: ${s.statusText}`);
159
+ if (s.error) console.log(`Error: ${s.error}`);
160
+ console.log(`Queue: ${s.queueLength} job(s)`);
161
+ if (s.workOffline) console.log(`Work offline flag: true`);
162
+ }
163
+
164
+ async function cmdConfig(opts: Record<string, string | boolean>): Promise<void> {
165
+ const hasUpdate = !!(opts.tape || opts.printer);
166
+ if (!hasUpdate) {
167
+ const cfg = getConfig();
168
+ console.log(`Configuration:`);
169
+ console.log(` File: ${getConfigPath()}`);
170
+ console.log(` Default tape: ${cfg.defaultTape ? cfg.defaultTape + "mm" : "(not set)"}`);
171
+ console.log(` Default printer: ${cfg.defaultPrinter ?? "(not set)"}`);
172
+ console.log("\nValid tape sizes: 6, 9, 12, 18, 24");
173
+ return;
174
+ }
175
+ if (opts.tape) {
176
+ const t = parseTape(opts.tape as string);
177
+ setConfig({ defaultTape: t });
178
+ console.log(`Default tape set to: ${t}mm`);
179
+ }
180
+ if (opts.printer) {
181
+ setConfig({ defaultPrinter: opts.printer as string });
182
+ console.log(`Default printer set to: ${opts.printer}`);
183
+ }
184
+ }
185
+
186
+ async function resolveTape(opts: Record<string, string | boolean>): Promise<TapeSize> {
187
+ const explicit = opts.tape ? parseTape(opts.tape as string) : null;
188
+ const detected = await detectTapeSize(opts.printer as string | undefined);
189
+ if (explicit) {
190
+ if (detected && detected !== explicit) {
191
+ console.warn(`Warning: -tape ${explicit}mm specified but printer reports ${detected}mm tape loaded`);
192
+ }
193
+ return explicit;
194
+ }
195
+ if (detected) return detected;
196
+ return getConfig().defaultTape ?? 24;
197
+ }
198
+
199
+ async function cmdPrint(
200
+ opts: Record<string, string | boolean>,
201
+ segments: Segment[],
202
+ inputs: string[]
203
+ ): Promise<void> {
204
+ const tape = await resolveTape(opts);
205
+
206
+ // If there are 2+ positional inputs, treat each as a text segment.
207
+ // (A single positional input is handled below via classifyInput so it can
208
+ // auto-detect file paths.)
209
+ if (inputs.length > 1 || (inputs.length >= 1 && segments.length > 0)) {
210
+ for (const inp of inputs) {
211
+ segments.push({ type: "text", value: inp });
212
+ }
213
+ inputs.length = 0;
214
+ }
215
+
216
+ const baseOpts = {
217
+ printer: opts.printer as string | undefined,
218
+ wait: !opts.nowait,
219
+ waitTimeoutMs: opts.timeout ? Math.round(parseFloat(opts.timeout as string) * 1000) : undefined,
220
+ waitIntervalMs: opts.interval ? Math.round(parseFloat(opts.interval as string) * 1000) : undefined,
221
+ onWaiting: makeWaitCallback(),
222
+ log: (msg: string) => console.log(msg),
223
+ };
224
+
225
+ // Multi-segment mode: explicit -text/-qr (>=1 segment), with positional joined in
226
+ if (segments.length > 1 || (segments.length === 1 && inputs.length === 0)) {
227
+ const segOpts: SegmentOptions = {
228
+ ...baseOpts,
229
+ tape,
230
+ textHeight: opts.height as string | undefined,
231
+ space: opts.space as string | undefined,
232
+ };
233
+ if (opts.output) {
234
+ const buffer = await renderSegments(segments, tape, opts.height as string | undefined, opts.space as string | undefined);
235
+ fs.writeFileSync(opts.output as string, buffer);
236
+ console.log(`Saved to ${opts.output}`);
237
+ } else {
238
+ await printSegments(segments, segOpts);
239
+ console.log(`Printed ${segments.length} segment(s) on ${tape}mm tape`);
240
+ }
241
+ return;
242
+ }
243
+
244
+ // Single content op
245
+ const printOpts: PrintOptions = {
246
+ ...baseOpts,
247
+ tape,
248
+ aspect: opts.aspect as string | undefined,
249
+ textHeight: opts.height as string | undefined,
250
+ };
251
+
252
+ if (opts.clip) {
253
+ printOpts.clip = true;
254
+ } else if (inputs.length === 1) {
255
+ Object.assign(printOpts, classifyInput(inputs[0], opts));
256
+ } else if (inputs.length === 0 && segments.length === 0) {
257
+ showHelp();
258
+ return;
259
+ }
260
+
261
+ if (opts.output) {
262
+ const buffer = await render(printOpts);
263
+ fs.writeFileSync(opts.output as string, buffer);
264
+ console.log(`Saved to ${opts.output}`);
265
+ return;
266
+ }
267
+ await print(printOpts);
268
+ console.log(`Printed on ${tape}mm tape`);
269
+ }
270
+
271
+ function classifyInput(input: string, opts: Record<string, string | boolean>): Partial<CorePrintOptions> {
272
+ if (opts.text) return { text: input };
273
+ if (opts.html) return { htmlPath: path.resolve(input) };
274
+ if (opts.image) return { imagePath: path.resolve(input) };
275
+
276
+ const lower = input.toLowerCase();
277
+ if (lower.endsWith(".html") || lower.endsWith(".htm")) return { htmlPath: path.resolve(input) };
278
+ if (lower.endsWith(".txt")) return { textFile: path.resolve(input) };
279
+ if (/\.(png|jpg|jpeg|bmp|gif)$/i.test(lower)) return { imagePath: path.resolve(input) };
280
+ if (fs.existsSync(input)) {
281
+ const ext = path.extname(lower);
282
+ if (ext === ".html" || ext === ".htm") return { htmlPath: path.resolve(input) };
283
+ return { textFile: path.resolve(input) };
284
+ }
285
+ return { text: input };
286
+ }
287
+
288
+ function makeWaitCallback() {
289
+ let lastReport = 0;
290
+ return (status: PrinterStatus, elapsedMs: number, alternatives: PrinterStatus[]) => {
291
+ const secs = Math.floor(elapsedMs / 1000);
292
+ if (secs - lastReport < 5) return;
293
+ lastReport = secs;
294
+ const altStr = alternatives.length > 0
295
+ ? ` (online alternatives: ${alternatives.map(a => a.name).join(", ")})`
296
+ : "";
297
+ console.log(`[brother-print] still waiting for ${status.name} (${status.statusText}${status.error ? ", " + status.error : ""}); ${secs}s elapsed${altStr}`);
298
+ };
299
+ }
300
+
301
+ main();