printer on atproto
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 236 lines 7.5 kB view raw
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}