|
|
const ESC = 0x1b
|
|
|
const GS = 0x1d
|
|
|
|
|
|
/**
|
|
|
* ESC/POS byte builder — pure, so receipt layout is testable without a
|
|
|
* printer. Spike scope is ASCII text mode; Indic scripts rasterize in
|
|
|
* bit-image mode later (09-UX §5.3 print policy).
|
|
|
*/
|
|
|
export class EscPos {
|
|
|
private bytes: number[] = []
|
|
|
|
|
|
init(): this {
|
|
|
this.bytes.push(ESC, 0x40)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
/** Non-ASCII collapses to '?' in text mode — raster path handles scripts. */
|
|
|
text(s: string): this {
|
|
|
for (const ch of s) {
|
|
|
const code = ch.codePointAt(0)!
|
|
|
this.bytes.push(code >= 0x20 && code <= 0x7e ? code : 0x3f)
|
|
|
}
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
line(s = ''): this {
|
|
|
return this.text(s).newline()
|
|
|
}
|
|
|
|
|
|
newline(): this {
|
|
|
this.bytes.push(0x0a)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
align(where: 'left' | 'center' | 'right'): this {
|
|
|
this.bytes.push(ESC, 0x61, where === 'left' ? 0 : where === 'center' ? 1 : 2)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
bold(on: boolean): this {
|
|
|
this.bytes.push(ESC, 0x45, on ? 1 : 0)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
/** Character cell multiplier 1–8 each axis (GS !). */
|
|
|
size(w: number, h: number): this {
|
|
|
this.bytes.push(GS, 0x21, ((w - 1) << 4) | (h - 1))
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
feed(lines: number): this {
|
|
|
this.bytes.push(ESC, 0x64, lines)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
/** Partial cut with pre-feed (GS V 66). */
|
|
|
cut(): this {
|
|
|
this.bytes.push(GS, 0x56, 66, 3)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
/** Cash-drawer kick pulse on pin 2 (ESC p). Fires on payment, not on print. */
|
|
|
drawerKick(): this {
|
|
|
this.bytes.push(ESC, 0x70, 0, 25, 250)
|
|
|
return this
|
|
|
}
|
|
|
|
|
|
build(): Uint8Array {
|
|
|
return Uint8Array.from(this.bytes)
|
|
|
}
|
|
|
}
|
|
|
|
|
|
/** Left/right column row padded to exactly `width` chars; left side truncates. */
|
|
|
export function twoCol(left: string, right: string, width: number): string {
|
|
|
const room = width - right.length - 1
|
|
|
const l = left.length > room ? left.slice(0, Math.max(0, room)) : left
|
|
|
return l + ' '.repeat(Math.max(1, width - l.length - right.length)) + right
|
|
|
}
|
|
|
|
|
|
export function hr(width: number): string {
|
|
|
return '-'.repeat(width)
|
|
|
}
|