printer on atproto
1use escpos::{
2 driver::{Driver, NetworkDriver, UsbDriver},
3 printer::Printer as EscposPrinter,
4 printer_options::PrinterOptions as EscposPrinterOpts,
5};
6use miette::{IntoDiagnostic, Result};
7
8type BoxedDriver = Box<dyn Driver + Send + 'static>;
9pub struct BoxDriver(BoxedDriver);
10
11impl Driver for BoxDriver {
12 fn name(&self) -> String {
13 self.0.name()
14 }
15
16 fn write(&self, data: &[u8]) -> escpos::errors::Result<()> {
17 self.0.write(data)
18 }
19
20 fn read(&self, buf: &mut [u8]) -> escpos::errors::Result<usize> {
21 self.0.read(buf)
22 }
23
24 fn flush(&self) -> escpos::errors::Result<()> {
25 self.0.flush()
26 }
27}
28
29pub enum PrinterEndpoint {
30 Usb { vendor_id: u16, product_id: u16 },
31 Network { host: String, port: u16 },
32}
33
34pub struct PrinterOptions {
35 pub chars_per_line: u8,
36}
37
38impl Default for PrinterOptions {
39 fn default() -> Self {
40 Self { chars_per_line: 48 }
41 }
42}
43
44pub struct Printer {
45 inner: EscposPrinter<BoxDriver>,
46 pub opts: PrinterOptions,
47 just_cut: bool,
48}
49
50impl Printer {
51 pub fn connect(endpoint: PrinterEndpoint, opts: PrinterOptions) -> Result<Self> {
52 let driver: BoxedDriver = match endpoint {
53 PrinterEndpoint::Usb {
54 vendor_id,
55 product_id,
56 } => Box::new(UsbDriver::open(vendor_id, product_id, None, None).into_diagnostic()?),
57 PrinterEndpoint::Network { host, port } => {
58 Box::new(NetworkDriver::open(&host, port, None).into_diagnostic()?)
59 }
60 };
61
62 let mut inner_opts = EscposPrinterOpts::default();
63 inner_opts.characters_per_line(opts.chars_per_line);
64 let mut inner = EscposPrinter::new(BoxDriver(driver), Default::default(), Some(inner_opts));
65 inner.init().into_diagnostic()?;
66
67 Ok(Self {
68 inner,
69 opts,
70 just_cut: true,
71 })
72 }
73
74 pub fn markdown(&mut self, text: impl AsRef<str>) -> Result<&mut Self> {
75 fn chunk_line(s: &str, width: usize) -> impl Iterator<Item = &str> {
76 let mut indices = s.char_indices().peekable();
77 std::iter::from_fn(move || {
78 let start = indices.peek()?.0;
79 indices.nth(width - 1); // advance width chars
80 let end = indices.peek().map(|(i, _)| *i).unwrap_or(s.len());
81 Some(&s[start..end])
82 })
83 }
84
85 fn wrap<'a>(s: &'a str, width: usize) -> impl Iterator<Item = &'a str> + 'a {
86 s.lines().flat_map(move |line| chunk_line(line, width))
87 }
88
89 use linemd::Parser;
90
91 let p = &mut self.inner;
92
93 let text = text.as_ref();
94 let md = text.parse_md();
95
96 #[derive(Default)]
97 struct RenderState {
98 was_line_break: bool,
99 in_paragraph: bool,
100 in_list: bool,
101 in_header: bool,
102 chars_per_line: usize,
103 }
104
105 fn write_token(
106 p: &mut EscposPrinter<BoxDriver>,
107 token: linemd::parser::Token,
108 state: &mut RenderState,
109 ) -> escpos::errors::Result<()> {
110 use linemd::parser::{Text as TextToken, Token::*};
111
112 let mut is_line_break = false;
113
114 match token {
115 Text(TextToken {
116 value, bold, code, ..
117 }) => {
118 p.bold(bold)?.reverse(code)?;
119 p.write(value)?;
120 p.bold(false)?.reverse(false)?;
121 state.in_paragraph = true;
122 }
123 Url { name, url, .. } => {
124 if let Some(name) = name {
125 write_token(p, Text(name), state)?;
126 p.write(&format!("({url})"))?;
127 } else {
128 p.write(url)?;
129 }
130 state.in_paragraph = true;
131 }
132 ListItem(idx) => {
133 match idx {
134 Some(idx) => p.write(&format!("{idx}. "))?,
135 None => p.write("- ")?,
136 };
137 state.in_paragraph = false;
138 state.in_list = true;
139 }
140 CodeFence { code, .. } => {
141 let width = state.chars_per_line;
142 p.reverse(true)?;
143 for line in wrap(code, width) {
144 p.write(&format!("{line:<width$}"))?;
145 }
146 p.reverse(false)?;
147 p.write("\n")?;
148 state.in_paragraph = false;
149 }
150 Header(depth) => {
151 let size = match depth {
152 1 => (2, 2),
153 2 => (2, 1),
154 3 => (1, 2),
155 _ => (1, 1),
156 };
157 p.size(size.0, size.1)?.bold(true)?;
158 state.in_paragraph = false;
159 state.in_header = true;
160 }
161 LineBreak => {
162 if state.was_line_break {
163 p.feed()?;
164 state.in_paragraph = false;
165 } else if state.in_paragraph && !state.in_list && !state.in_header {
166 p.write(" ")?;
167 } else {
168 if state.in_header {
169 p.reset_size()?.bold(false)?;
170 p.write("\n")?;
171 }
172 p.write("\n")?;
173 }
174 is_line_break = true;
175 state.in_list = false;
176 state.in_header = false;
177 }
178 Custom(_) => {}
179 }
180
181 state.was_line_break = is_line_break;
182 Ok(())
183 }
184
185 if !self.just_cut {
186 p.feed().into_diagnostic()?;
187 }
188 let mut state = RenderState::default();
189 state.chars_per_line = self.opts.chars_per_line as usize;
190 for token in md {
191 write_token(p, token, &mut state).into_diagnostic()?;
192 }
193 p.print().into_diagnostic()?;
194
195 self.just_cut = false;
196
197 Ok(self)
198 }
199
200 pub fn cut(&mut self) -> Result<&mut Self> {
201 self.inner
202 .cut()
203 .into_diagnostic()?
204 .print()
205 .into_diagnostic()?;
206 self.just_cut = true;
207 Ok(self)
208 }
209
210 pub fn centered(&mut self, text: &str, fill: u8) -> Result<&mut Self> {
211 let width = self.opts.chars_per_line as usize;
212 let text = text.trim();
213 let padded = format!(" {text} ");
214 let fill_total = width.saturating_sub(padded.len());
215 let left: Vec<u8> = std::iter::repeat(fill).take(fill_total / 2).collect();
216 let right: Vec<u8> = std::iter::repeat(fill)
217 .take(fill_total - fill_total / 2)
218 .collect();
219 let p = &mut self.inner;
220 p.custom(&left).into_diagnostic()?;
221 p.write(&padded).into_diagnostic()?;
222 p.custom(&right).into_diagnostic()?;
223 p.write("\n").into_diagnostic()?;
224 p.print().into_diagnostic()?;
225 Ok(self)
226 }
227
228 pub fn newline(&mut self) -> Result<&mut Self> {
229 self.inner
230 .write("\n")
231 .into_diagnostic()?
232 .print()
233 .into_diagnostic()?;
234 Ok(self)
235 }
236}