diff --git a/Cargo.lock b/Cargo.lock index d822887..340bf6d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2457,6 +2457,11 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linemd" +version = "0.4.0" +source = "git+https://github.com/90-008/linemd#e97975b8e12bd0963dae492e64e54f95dcd70085" + [[package]] name = "linked-hash-map" version = "0.5.6" @@ -4281,6 +4286,7 @@ dependencies = [ "hydrant", "jacquard-common", "jacquard-derive", + "linemd", "miette", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index d83a976..4839e94 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,3 +15,16 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" jacquard-derive = "0.11.0" jacquard-common = { version = "0.11.0", default-features = false, features = ["std"] } +linemd = { git = "https://github.com/90-008/linemd", default-features = false } + +[profile.dev] +opt-level = 1 + +[profile.dev.package.hydrant] +opt-level = 3 + +[profile.dev.package.fjall] +opt-level = 2 + +[profile.dev.package.lsm-tree] +opt-level = 2 diff --git a/src/main.rs b/src/main.rs index a459e74..495e594 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ -use std::borrow::Cow; - use futures::StreamExt; use hydrant::control::Hydrant; -use jacquard_common::types::did::Did; +use jacquard_common::{ + deps::chrono::{DateTime, Utc}, + types::{did::Did, tid::Tid}, +}; use miette::{IntoDiagnostic, Result}; use serde::{Deserialize, Serialize}; use tokio::sync::mpsc; @@ -37,6 +38,7 @@ struct TextContent { } struct Task { + date: DateTime, identifier: String, job: Job, } @@ -67,16 +69,16 @@ async fn main() -> Result<()> { let hydrant = Hydrant::from_env().await?; hydrant.crawler.disable(); - hydrant.filter.add_collection(LEX); + hydrant.filter.add_collection(LEX).apply().await?; hydrant.repos.track(allowed_dids).await?; let (task_tx, task_rx) = mpsc::channel(16); let job_handler = tokio::task::spawn_blocking(move || handle_jobs(printer, task_rx)); - let job_stream = stream_jobs(hydrant.clone(), task_tx); + let job_stream = tokio::spawn(stream_jobs(hydrant.clone(), task_tx)); tokio::select! { r = hydrant.run()? => r, - r = job_stream => r, + r = job_stream => r.into_diagnostic().flatten(), r = job_handler => r.into_diagnostic().flatten(), } } @@ -84,31 +86,43 @@ async fn main() -> Result<()> { async fn stream_jobs(hydrant: Hydrant, task_tx: TaskTx) -> Result<()> { let mut stream = hydrant.subscribe(None); while let Some(event) = stream.next().await { - let Some(record) = event.record else { + let Some(event) = event.record else { continue; }; - if !record.live || record.action != "create" || record.collection.as_str() != LEX { + if !event.live || event.action != "create" || event.collection.as_str() != LEX { // event we dont care about continue; } - let Some(value) = record.record else { + let Some(record) = event.record else { continue; }; - let job: Job = match serde_json::from_value(value) { + let job: Job = match serde_json::from_value(record) { Ok(j) => j, Err(e) => { tracing::warn!(err = %e, "invalid record"); continue; } }; - let Some(repo) = hydrant.repos.info(&record.did).await? else { + let Some(repo) = hydrant.repos.info(&event.did).await? else { continue; }; let identifier = repo .handle .map(|h| h.to_string()) .unwrap_or_else(|| repo.did.to_string()); - let _ = task_tx.send(Task { identifier, job }).await; + let Some(date) = Tid::new(event.rkey) + .ok() + .and_then(|tid| DateTime::from_timestamp_micros(tid.timestamp() as i64)) + else { + continue; + }; + let _ = task_tx + .send(Task { + identifier, + job, + date, + }) + .await; } Ok(()) } @@ -117,8 +131,12 @@ fn handle_jobs(mut printer: Printer, mut task_rx: TaskRx) -> Result<()> { while let Some(task) = task_rx.blocking_recv() { match task.job.content { JobContent::Text(c) if !c.text.is_empty() => { - let final_text = format!("[{}] {}", task.identifier, c.text); - printer.print(&final_text)?.cut()?; + printer + .centered(&task.identifier, 0xCD)? + .markdown(&c.text)? + .centered(&task.date.to_string(), 0xCD)? + .newline()? + .cut()?; } _ => continue, } diff --git a/src/printer.rs b/src/printer.rs index 1a69b9e..fe38794 100644 --- a/src/printer.rs +++ b/src/printer.rs @@ -37,7 +37,7 @@ pub struct PrinterOptions { impl Default for PrinterOptions { fn default() -> Self { - Self { chars_per_line: 42 } + Self { chars_per_line: 48 } } } @@ -71,31 +71,126 @@ impl Printer { }) } - pub fn print(&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]) - // }) - // } - - // let lines = text - // .as_ref() - // .lines() - // .flat_map(|line| chunk_line(line, self.opts.chars_per_line as usize)); + 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]) + }) + } - if !self.just_cut { - self.inner.feed().into_diagnostic()?; + fn wrap<'a>(s: &'a str, width: usize) -> impl Iterator + 'a { + s.lines().flat_map(move |line| chunk_line(line, width)) } - self.inner - .write(text.as_ref()) - .into_diagnostic()? - .print() - .into_diagnostic()?; + 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; @@ -111,4 +206,31 @@ impl Printer { 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) + } }