import { Printer as EscposPrinter } from '@node-escpos/core'; import { formatUtils } from './format-utils'; import { Command, CommandTuple, CommandArray } from './printer-commands'; import { FONT_SIZES, ALIGNMENT, FONT, STYLE, BARCODE_CONFIG } from './printer-constants'; import logger from '../logger'; export interface CommandExecutor { executeCommands(commands: CommandArray): Promise; } export class SerialCommandExecutor implements CommandExecutor { constructor(private printer: EscposPrinter<[]>) {} async executeCommands(commands: CommandArray): Promise { for (const command of commands) { await this.executeCommand(command); } } private async executeCommand(commandTuple: CommandTuple): Promise { const [command, ...params] = commandTuple; switch (command) { case Command.TEXT: await this.printer.text(params[0] as string); break; case Command.HEADER: await this.printer .font(FONT.DEFAULT) .align(ALIGNMENT.CENTER) .style(STYLE.BOLD) .size(FONT_SIZES.LARGE.width, FONT_SIZES.LARGE.height) .text(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); break; case Command.BANNER: const char = params[0] as string; const length = params[1] as number; await this.printer.text(formatUtils.bannerString(char, length)); break; case Command.NEWLINE: await this.printer.text(''); break; case Command.BARCODE: const barcodeData = params[0] as string; logger.info(`Printing barcode: ${barcodeData} with type: ${BARCODE_CONFIG.TYPE}`); // Try multiple barcode formats until one works const barcodeTypes = [BARCODE_CONFIG.TYPE, ...BARCODE_CONFIG.ALTERNATIVE_TYPES]; let barcodePrinted = false; for (const barcodeType of barcodeTypes) { try { logger.info(`Trying barcode type: ${barcodeType}`); await this.printer.barcode( barcodeData, barcodeType, BARCODE_CONFIG.DIMENSIONS ); logger.info(`Successfully printed barcode with type: ${barcodeType}`); barcodePrinted = true; break; } catch (error) { logger.warn(`Barcode type ${barcodeType} failed: ${error}`); continue; } } if (!barcodePrinted) { logger.error(`All barcode types failed for data: ${barcodeData}`); // As a last resort, just print the data as text await this.printer.text(`[BARCODE: ${barcodeData}]`); } break; case Command.FONT_SIZE: const size = params[0] as typeof FONT_SIZES[keyof typeof FONT_SIZES]; await this.printer.size(size.width, size.height); break; case Command.ALIGN: await this.printer.align(params[0] as any); break; case Command.FONT_FAMILY: await this.printer.font(params[0] as any); break; case Command.STYLE: await this.printer.style(params[0] as any); break; case Command.CUT: const partial = params[0] as boolean; const lines = params[1] as number; await this.printer.cut(partial, lines); break; case Command.SECTION: const header = params[0] as string; const content = params[1] as string; const bannerChar = params[2] as string; const trailingNewline = params[3] as boolean; const section = formatUtils.section(header, content, bannerChar, trailingNewline); for (const cmd of section) { if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) { await this.printer.text(cmd[1] as string); } else if (cmd[0] === Command.BANNER) { await this.printer.text(formatUtils.bannerString(cmd[1] as string, cmd[2] as number)); } else if (cmd[0] === Command.NEWLINE) { await this.printer.text(''); } } break; case Command.STEP_HEADER: const stepName = params[0] as string; const stepNumber = params[1] as number; const taskName = params[2] as string; const isTaskView = params[3] as boolean; const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView); await this.printer.text(formatUtils.getCheckboxText(stepHeader)); break; case Command.CHECKBOX: const text = params[0] as string; const checkbox = `[ ] ${text}`; await this.printer.text(checkbox); break; case Command.LIST: const items = params[0] as string[]; const startIndex = params[1] as number; const prefix = params[2] as string; const listCmds = formatUtils.list(items, startIndex, prefix); for (const cmd of listCmds) { if (cmd[0] === Command.LIST) { // Render as text for test printer, or as needed for real printer for (let i = 0; i < items.length; i++) { const num = startIndex > 0 ? `${i + startIndex}: ` : ''; const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`)); await this.printer.text(`${prefix}${body}`); } } } break; default: throw new Error(`Unknown command: ${command}`); } } } export class TestCommandExecutor implements CommandExecutor { private output: string[] = []; async executeCommands(commands: CommandArray): Promise { this.output = []; for (const command of commands) { await this.executeCommand(command); } } private async executeCommand(commandTuple: CommandTuple): Promise { const [command, ...params] = commandTuple; switch (command) { case Command.TEXT: this.output.push(params[0] as string); break; case Command.HEADER: this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); break; case Command.BANNER: const char = params[0] as string; const length = params[1] as number; this.output.push(formatUtils.banner(char, length)[1] as string); break; case Command.NEWLINE: this.output.push(''); break; case Command.BARCODE: this.output.push(`[BARCODE: ${params[0] as string}]`); break; case Command.FONT_SIZE: // Test printer ignores font size changes break; case Command.ALIGN: // Test printer ignores alignment changes break; case Command.FONT_FAMILY: // Test printer ignores font changes break; case Command.STYLE: // Test printer ignores style changes break; case Command.CUT: this.output.push('--- CUT ---'); break; case Command.SECTION: const header = params[0] as string; const content = params[1] as string; const bannerChar = params[2] as string; const trailingNewline = params[3] as boolean; const section = formatUtils.section(header, content, bannerChar, trailingNewline); for (const cmd of section) { if (cmd[0] === Command.CHECKBOX || cmd[0] === Command.TEXT) { this.output.push(cmd[1] as string); } else if (cmd[0] === Command.BANNER) { this.output.push(formatUtils.bannerString(cmd[1] as string, cmd[2] as number)); } else if (cmd[0] === Command.NEWLINE) { this.output.push(''); } } break; case Command.STEP_HEADER: const stepName = params[0] as string; const stepNumber = params[1] as number; const taskName = params[2] as string; const isTaskView = params[3] as boolean; const stepHeader = formatUtils.stepHeader(stepName, stepNumber, taskName, isTaskView); this.output.push(formatUtils.getCheckboxText(stepHeader)); break; case Command.CHECKBOX: this.output.push(formatUtils.getCheckboxText(formatUtils.checkbox(params[0] as string))); break; case Command.LIST: const items = params[0] as string[]; const startIndex = params[1] as number; const prefix = params[2] as string; const listCmds = formatUtils.list(items, startIndex, prefix); for (const cmd of listCmds) { if (cmd[0] === Command.LIST) { for (let i = 0; i < items.length; i++) { const num = startIndex > 0 ? `${i + startIndex}: ` : ''; const body = formatUtils.getCheckboxText(formatUtils.checkbox(`${num}${items[i]}`)); this.output.push(`${prefix}${body}`); } } } break; default: throw new Error(`Unknown command: ${command}`); } } getOutput(): string { return this.output.join('\n') + '\n'; } }