Something went wrong. Try again.
printer on atproto
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237use escpos::{ driver::{Driver, NetworkDriver, UsbDriver}, printer::Printer as EscposPrinter, printer_options::PrinterOptions as EscposPrinterOpts,};use miette::{IntoDiagnostic, Result};
type BoxedDriver = Box<dyn Driver + Send + 'static>;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<usize> { 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<BoxDriver>, pub opts: PrinterOptions, just_cut: bool,}
impl Printer { pub fn connect(endpoint: PrinterEndpoint, opts: PrinterOptions) -> Result<Self> { 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<str>) -> Result<&mut Self> { fn chunk_line(s: &str, width: usize) -> impl Iterator<Item = &str> { 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<Item = &'a str> + '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<BoxDriver>, 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:<width$}"))?; } p.reverse(false)?; p.write("\n")?; state.in_paragraph = false; } Header(depth) => { 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<u8> = std::iter::repeat(fill).take(fill_total / 2).collect(); let right: Vec<u8> = 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) }}