printer on atproto
4
fork

Configure Feed

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

improve the printing a looooot

dawn 300f73bf e7e40b67

+195 -36
+6
Cargo.lock
··· 2458 2458 ] 2459 2459 2460 2460 [[package]] 2461 + name = "linemd" 2462 + version = "0.4.0" 2463 + source = "git+https://github.com/90-008/linemd#e97975b8e12bd0963dae492e64e54f95dcd70085" 2464 + 2465 + [[package]] 2461 2466 name = "linked-hash-map" 2462 2467 version = "0.5.6" 2463 2468 source = "registry+https://github.com/rust-lang/crates.io-index" ··· 4281 4286 "hydrant", 4282 4287 "jacquard-common", 4283 4288 "jacquard-derive", 4289 + "linemd", 4284 4290 "miette", 4285 4291 "serde", 4286 4292 "serde_json",
+13
Cargo.toml
··· 15 15 serde_json = "1.0.149" 16 16 jacquard-derive = "0.11.0" 17 17 jacquard-common = { version = "0.11.0", default-features = false, features = ["std"] } 18 + linemd = { git = "https://github.com/90-008/linemd", default-features = false } 19 + 20 + [profile.dev] 21 + opt-level = 1 22 + 23 + [profile.dev.package.hydrant] 24 + opt-level = 3 25 + 26 + [profile.dev.package.fjall] 27 + opt-level = 2 28 + 29 + [profile.dev.package.lsm-tree] 30 + opt-level = 2
+32 -14
src/main.rs
··· 1 - use std::borrow::Cow; 2 - 3 1 use futures::StreamExt; 4 2 use hydrant::control::Hydrant; 5 - use jacquard_common::types::did::Did; 3 + use jacquard_common::{ 4 + deps::chrono::{DateTime, Utc}, 5 + types::{did::Did, tid::Tid}, 6 + }; 6 7 use miette::{IntoDiagnostic, Result}; 7 8 use serde::{Deserialize, Serialize}; 8 9 use tokio::sync::mpsc; ··· 37 38 } 38 39 39 40 struct Task { 41 + date: DateTime<Utc>, 40 42 identifier: String, 41 43 job: Job, 42 44 } ··· 67 69 68 70 let hydrant = Hydrant::from_env().await?; 69 71 hydrant.crawler.disable(); 70 - hydrant.filter.add_collection(LEX); 72 + hydrant.filter.add_collection(LEX).apply().await?; 71 73 hydrant.repos.track(allowed_dids).await?; 72 74 73 75 let (task_tx, task_rx) = mpsc::channel(16); 74 76 let job_handler = tokio::task::spawn_blocking(move || handle_jobs(printer, task_rx)); 75 - let job_stream = stream_jobs(hydrant.clone(), task_tx); 77 + let job_stream = tokio::spawn(stream_jobs(hydrant.clone(), task_tx)); 76 78 77 79 tokio::select! { 78 80 r = hydrant.run()? => r, 79 - r = job_stream => r, 81 + r = job_stream => r.into_diagnostic().flatten(), 80 82 r = job_handler => r.into_diagnostic().flatten(), 81 83 } 82 84 } ··· 84 86 async fn stream_jobs(hydrant: Hydrant, task_tx: TaskTx) -> Result<()> { 85 87 let mut stream = hydrant.subscribe(None); 86 88 while let Some(event) = stream.next().await { 87 - let Some(record) = event.record else { 89 + let Some(event) = event.record else { 88 90 continue; 89 91 }; 90 - if !record.live || record.action != "create" || record.collection.as_str() != LEX { 92 + if !event.live || event.action != "create" || event.collection.as_str() != LEX { 91 93 // event we dont care about 92 94 continue; 93 95 } 94 - let Some(value) = record.record else { 96 + let Some(record) = event.record else { 95 97 continue; 96 98 }; 97 - let job: Job = match serde_json::from_value(value) { 99 + let job: Job = match serde_json::from_value(record) { 98 100 Ok(j) => j, 99 101 Err(e) => { 100 102 tracing::warn!(err = %e, "invalid record"); 101 103 continue; 102 104 } 103 105 }; 104 - let Some(repo) = hydrant.repos.info(&record.did).await? else { 106 + let Some(repo) = hydrant.repos.info(&event.did).await? else { 105 107 continue; 106 108 }; 107 109 let identifier = repo 108 110 .handle 109 111 .map(|h| h.to_string()) 110 112 .unwrap_or_else(|| repo.did.to_string()); 111 - let _ = task_tx.send(Task { identifier, job }).await; 113 + let Some(date) = Tid::new(event.rkey) 114 + .ok() 115 + .and_then(|tid| DateTime::from_timestamp_micros(tid.timestamp() as i64)) 116 + else { 117 + continue; 118 + }; 119 + let _ = task_tx 120 + .send(Task { 121 + identifier, 122 + job, 123 + date, 124 + }) 125 + .await; 112 126 } 113 127 Ok(()) 114 128 } ··· 117 131 while let Some(task) = task_rx.blocking_recv() { 118 132 match task.job.content { 119 133 JobContent::Text(c) if !c.text.is_empty() => { 120 - let final_text = format!("[{}] {}", task.identifier, c.text); 121 - printer.print(&final_text)?.cut()?; 134 + printer 135 + .centered(&task.identifier, 0xCD)? 136 + .markdown(&c.text)? 137 + .centered(&task.date.to_string(), 0xCD)? 138 + .newline()? 139 + .cut()?; 122 140 } 123 141 _ => continue, 124 142 }
+144 -22
src/printer.rs
··· 37 37 38 38 impl Default for PrinterOptions { 39 39 fn default() -> Self { 40 - Self { chars_per_line: 42 } 40 + Self { chars_per_line: 48 } 41 41 } 42 42 } 43 43 ··· 71 71 }) 72 72 } 73 73 74 - pub fn print(&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 - // } 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 84 85 - // let lines = text 86 - // .as_ref() 87 - // .lines() 88 - // .flat_map(|line| chunk_line(line, self.opts.chars_per_line as usize)); 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 + } 89 88 90 - if !self.just_cut { 91 - self.inner.feed().into_diagnostic()?; 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, 92 103 } 93 104 94 - self.inner 95 - .write(text.as_ref()) 96 - .into_diagnostic()? 97 - .print() 98 - .into_diagnostic()?; 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()?; 99 194 100 195 self.just_cut = false; 101 196 ··· 109 204 .print() 110 205 .into_diagnostic()?; 111 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()?; 112 234 Ok(self) 113 235 } 114 236 }