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