use escpos::{ driver::{Driver, NetworkDriver, UsbDriver}, printer::Printer as EscposPrinter, printer_options::PrinterOptions as EscposPrinterOpts, }; use miette::{IntoDiagnostic, Result}; type BoxedDriver = Box; pub struct BoxDriver(BoxedDriver); impl Driver for BoxDriver { fn name(&self) -> String { self.0.name() } fn write(&self, data: &[u8]) -> escpos::errors::Result<()> { self.0.write(data) } fn read(&self, buf: &mut [u8]) -> escpos::errors::Result { self.0.read(buf) } fn flush(&self) -> escpos::errors::Result<()> { self.0.flush() } } pub enum PrinterEndpoint { Usb { vendor_id: u16, product_id: u16 }, Network { host: String, port: u16 }, } pub struct PrinterOptions { pub chars_per_line: u8, } impl Default for PrinterOptions { fn default() -> Self { Self { chars_per_line: 48 } } } pub struct Printer { inner: EscposPrinter, pub opts: PrinterOptions, just_cut: bool, } impl Printer { pub fn connect(endpoint: PrinterEndpoint, opts: PrinterOptions) -> Result { let driver: BoxedDriver = match endpoint { PrinterEndpoint::Usb { vendor_id, product_id, } => Box::new(UsbDriver::open(vendor_id, product_id, None, None).into_diagnostic()?), PrinterEndpoint::Network { host, port } => { Box::new(NetworkDriver::open(&host, port, None).into_diagnostic()?) } }; let mut inner_opts = EscposPrinterOpts::default(); inner_opts.characters_per_line(opts.chars_per_line); let mut inner = EscposPrinter::new(BoxDriver(driver), Default::default(), Some(inner_opts)); inner.init().into_diagnostic()?; Ok(Self { inner, opts, just_cut: true, }) } pub fn markdown(&mut self, text: impl AsRef) -> Result<&mut Self> { fn chunk_line(s: &str, width: usize) -> impl Iterator { let mut indices = s.char_indices().peekable(); std::iter::from_fn(move || { let start = indices.peek()?.0; indices.nth(width - 1); // advance width chars let end = indices.peek().map(|(i, _)| *i).unwrap_or(s.len()); Some(&s[start..end]) }) } fn wrap<'a>(s: &'a str, width: usize) -> impl Iterator + 'a { s.lines().flat_map(move |line| chunk_line(line, width)) } use linemd::Parser; let p = &mut self.inner; let text = text.as_ref(); let md = text.parse_md(); #[derive(Default)] struct RenderState { was_line_break: bool, in_paragraph: bool, in_list: bool, in_header: bool, chars_per_line: usize, } fn write_token( p: &mut EscposPrinter, token: linemd::parser::Token, state: &mut RenderState, ) -> escpos::errors::Result<()> { use linemd::parser::{Text as TextToken, Token::*}; let mut is_line_break = false; match token { Text(TextToken { value, bold, code, .. }) => { p.bold(bold)?.reverse(code)?; p.write(value)?; p.bold(false)?.reverse(false)?; state.in_paragraph = true; } Url { name, url, .. } => { if let Some(name) = name { write_token(p, Text(name), state)?; p.write(&format!("({url})"))?; } else { p.write(url)?; } state.in_paragraph = true; } ListItem(idx) => { match idx { Some(idx) => p.write(&format!("{idx}. "))?, None => p.write("- ")?, }; state.in_paragraph = false; state.in_list = true; } CodeFence { code, .. } => { let width = state.chars_per_line; p.reverse(true)?; for line in wrap(code, width) { p.write(&format!("{line: { let size = match depth { 1 => (2, 2), 2 => (2, 1), 3 => (1, 2), _ => (1, 1), }; p.size(size.0, size.1)?.bold(true)?; state.in_paragraph = false; state.in_header = true; } LineBreak => { if state.was_line_break { p.feed()?; state.in_paragraph = false; } else if state.in_paragraph && !state.in_list && !state.in_header { p.write(" ")?; } else { if state.in_header { p.reset_size()?.bold(false)?; p.write("\n")?; } p.write("\n")?; } is_line_break = true; state.in_list = false; state.in_header = false; } Custom(_) => {} } state.was_line_break = is_line_break; Ok(()) } if !self.just_cut { p.feed().into_diagnostic()?; } let mut state = RenderState::default(); state.chars_per_line = self.opts.chars_per_line as usize; for token in md { write_token(p, token, &mut state).into_diagnostic()?; } p.print().into_diagnostic()?; self.just_cut = false; Ok(self) } pub fn cut(&mut self) -> Result<&mut Self> { self.inner .cut() .into_diagnostic()? .print() .into_diagnostic()?; self.just_cut = true; Ok(self) } pub fn centered(&mut self, text: &str, fill: u8) -> Result<&mut Self> { let width = self.opts.chars_per_line as usize; let text = text.trim(); let padded = format!(" {text} "); let fill_total = width.saturating_sub(padded.len()); let left: Vec = std::iter::repeat(fill).take(fill_total / 2).collect(); let right: Vec = std::iter::repeat(fill) .take(fill_total - fill_total / 2) .collect(); let p = &mut self.inner; p.custom(&left).into_diagnostic()?; p.write(&padded).into_diagnostic()?; p.custom(&right).into_diagnostic()?; p.write("\n").into_diagnostic()?; p.print().into_diagnostic()?; Ok(self) } pub fn newline(&mut self) -> Result<&mut Self> { self.inner .write("\n") .into_diagnostic()? .print() .into_diagnostic()?; Ok(self) } }