lmjtfy.git / packages / card / src / lib.rs
lib.rsannotatedlib.rssource333 lines · 13.5 KB · raw

The picture a link unfurls with: the question and Jev's answer, drawn the way the page looks, as a PNG. No I/O.

It is drawn by hand, a pixel at a time, in an 8 by 8 bitmap face scaled up. That is the page's look (dither, window frames, pixel type), and it needs no font engine: a Worker has ten milliseconds to make this, and a megabyte or two less to carry. Four colours, two bits a pixel.

8#![forbid(unsafe_code)]
10use archive::{Answer, Detail};
11use font8x8::UnicodeFonts;
12
13pub const WIDTH: usize = 600;
14pub const HEIGHT: usize = 315;

The palette, as the page's --paper, --ink, --pink and --grey.

17const PALETTE: [u8; 12] = [0xfe, 0xfe, 0xfe, 0x1e, 0x1e, 0x1e, 0xf3, 0x86, 0xa1, 0xde, 0xde, 0xde];
18const PAPER: u8 = 0;
19const INK: u8 = 1;
20const PINK: u8 = 2;
21const GREY: u8 = 3;

What the card is of.

24#[derive(Clone, Copy, Debug, PartialEq)]
25pub enum Card<'a> {

The site itself.

27    Home,

A question, and what Jev said: nothing, if it has not been asked.

29    Question { input: &'a str, said: &'a [Answer] },

A repository's code: its name (lmjtfy.git), the clone command for url (with submodules when it has some), what it is, and the latest commit if it could be read.

33    Code { name: &'a str, url: &'a str, recurse: bool, about: &'a str, branch: &'a str, latest: Option<&'a Commit> },
34}

The latest commit on the code's main branch, as the card and the page show it.

38#[derive(Clone, Debug, PartialEq)]
39pub struct Commit {

The full hash.

41    pub sha: String,

The message's first line.

43    pub subject: String,

When it was committed, as YYYY-MM-DD.

45    pub date: String,

How many commits main has, if GitHub said.

47    pub count: Option<u64>,
48}
50impl Commit {
51    pub fn short(&self) -> &str {
52        self.sha.get(..7).unwrap_or(&self.sha)
53    }

4d93353 · 2026-10-02 · 140 commits.

56    pub fn line(&self) -> String {
57        let mut parts = vec![self.short().to_owned(), self.date.clone()];
58        parts.extend(self.count.map(|count| format!("{count} commits")));
59        parts.join(" · ")
60    }
61}
63struct Canvas {
64    pixels: Vec<u8>,
65}

A 4 by 4 ordered dither: how dark a field has to be before each cell of the pattern takes ink.

69const BAYER: [[u8; 4]; 4] = [[0, 8, 2, 10], [12, 4, 14, 6], [3, 11, 1, 9], [15, 7, 13, 5]];
71impl Canvas {
72    fn new() -> Self {
73        Canvas { pixels: vec![PAPER; WIDTH * HEIGHT] }
74    }
75
76    fn fill(&mut self, x: usize, y: usize, w: usize, h: usize, colour: u8) {
77        for row in y..(y + h).min(HEIGHT) {
78            for column in x..(x + w).min(WIDTH) {
79                self.pixels[row * WIDTH + column] = colour;
80            }
81        }
82    }
83
84    fn frame(&mut self, x: usize, y: usize, w: usize, h: usize, colour: u8) {
85        self.fill(x, y, w, 1, colour);
86        self.fill(x, y + h - 1, w, 1, colour);
87        self.fill(x, y, 1, h, colour);
88        self.fill(x + w - 1, y, 1, h, colour);
89    }

The page's dot field: dense at the top left and bottom right corners, thinning to nothing across the middle. Dots are 2 by 2.

93    fn dither(&mut self, x0: usize, y0: usize, w: usize, h: usize, colour: u8) {
94        for y in (y0..y0 + h).step_by(2) {
95            for x in (x0..x0 + w).step_by(2) {
96                let across = (x - x0) as f64 / w as f64;
97                let down = (y - y0) as f64 / h as f64;
98                let from_corner = (across + down).min(2.0 - across - down);
99                let dark = (1.0 - from_corner * 1.6).max(0.0) * 16.0;
100                if f64::from(BAYER[(y / 2) % 4][(x / 2) % 4]) < dark {
101                    self.fill(x, y, 2, 2, colour);
102                }
103            }
104        }
105    }

One line of text, each font pixel scale across. Returns its width.

108    fn text(&mut self, x: usize, y: usize, text: &str, scale: usize, colour: u8) -> usize {
109        for (index, ch) in text.chars().enumerate() {
110            let glyph = glyph(ch);
111            for (row, bits) in glyph.iter().enumerate() {
112                for column in 0..8 {
113                    if bits >> column & 1 == 1 {
114                        self.fill(x + (index * 8 + column) * scale, y + row * scale, scale, scale, colour);
115                    }
116                }
117            }
118        }
119        width(text, scale)
120    }
121}

A character's 8 rows, bit 0 leftmost. One the face lacks is drawn as ?.

124fn glyph(ch: char) -> [u8; 8] {
125    font8x8::BASIC_FONTS
126        .get(ch)
127        .or_else(|| font8x8::LATIN_FONTS.get(ch))
128        .or_else(|| font8x8::BASIC_FONTS.get('?'))
129        .unwrap_or([0; 8])
130}
132fn width(text: &str, scale: usize) -> usize {
133    text.chars().count() * 8 * scale
134}

text cut to chars, the cut marked.

137fn cut(text: &str, chars: usize) -> String {
138    if text.chars().count() <= chars {
139        return text.to_owned();
140    }
141    let kept: String = text.chars().take(chars.saturating_sub(3)).collect();
142    format!("{}...", kept.trim_end())
143}

text broken at spaces into at most lines lines of at most chars, the last cut if there is more. A word longer than a line is cut.

147fn wrap(text: &str, chars: usize, lines: usize) -> Vec<String> {
148    let mut wrapped: Vec<String> = Vec::new();
149    let mut words = text.split_whitespace().peekable();
150    while let Some(word) = words.next() {
151        let fits = wrapped.last().is_some_and(|line| line.chars().count() + 1 + word.chars().count() <= chars);
152        if fits {
153            let line = wrapped.last_mut().expect("a line was seen to fit");
154            line.push(' ');
155            line.push_str(word);
156        } else if wrapped.len() < lines {
157            wrapped.push(cut(word, chars));
158        } else {
159            // No room for another line: the last one says there was more.
160            let last = wrapped.last_mut().expect("lines is at least one");
161            *last = cut(&format!("{last} {word}"), chars);
162            if words.peek().is_some() && !last.ends_with("...") {
163                *last = cut(&format!("{last} ..."), chars);
164            }
165            break;
166        }
167    }
168    wrapped
169}

The largest scale, at most most, at which text is no wider than room.

172fn fit(text: &str, room: usize, most: usize) -> usize {
173    (room / (text.chars().count().max(1) * 8)).clamp(1, most)
174}
176const MARGIN: usize = 20;
177const BAR: usize = 28;
178const BAND: usize = 40;
179
180fn draw(card: Card<'_>) -> Canvas {
181    let mut canvas = Canvas::new();
182    canvas.dither(0, 0, WIDTH, HEIGHT - BAND, GREY);
183    // The pink band along the bottom, with its own darker dots.
184    canvas.fill(0, HEIGHT - BAND, WIDTH, BAND, PINK);
185    canvas.dither(0, HEIGHT - BAND, WIDTH, BAND, INK);
186    let site = "LET ME JEV THAT FOR YOU";
187    let site_x = (WIDTH - width(site, 2)) / 2;
188    canvas.fill(site_x - 8, HEIGHT - BAND + 8, width(site, 2) + 16, 24, PINK);
189    canvas.text(site_x, HEIGHT - BAND + 12, site, 2, INK);
190
191    // The window.
192    let (x, y) = (MARGIN, MARGIN);
193    let (w, h) = (WIDTH - 2 * MARGIN, HEIGHT - BAND - 2 * MARGIN);
194    canvas.fill(x, y, w, h, PAPER);
195    canvas.frame(x, y, w, h, INK);
196    canvas.fill(x, y, w, BAR, INK);
197    let title = if let Card::Code { name, .. } = card { name } else { "Jev" };
198    canvas.text(x + 10, y + 6, title, 2, PAPER);
199    let inner = x + 16;
200    let room = w - 32;
201    let top = y + BAR + 14;
202
203    match card {
204        Card::Home => {
205            let scale = 5;
206            for (line, words) in ["Let Me Jev", "That For You"].iter().enumerate() {
207                canvas.text(inner + (room - width(words, scale)) / 2, top + 22 + line * 52, words, scale, INK);
208            }
209            let ask = "Ask it anything. It says yes, no, which or how much.";
210            canvas.text(inner + (room - width(ask, 1)) / 2, top + 140, ask, 1, INK);
211        }
212        Card::Question { input, said } => {
213            // The question, small, up to two lines.
214            let label = canvas.text(inner, top, "Q:", 2, INK) + 12;
215            let lines = wrap(input, (room - label) / 16, 2);
216            for (line, words) in lines.iter().enumerate() {
217                canvas.text(inner + label, top + line * 20, words, 2, INK);
218            }
219            let mut at = top + lines.len().max(1) * 20 + 10;
220            if said.is_empty() {
221                canvas.text(inner, at + 30, "Let me Jev that for you.", 2, INK);
222                return canvas;
223            }
224            // The answer, as large as fits the width, up to 48 high.
225            let headlines: Vec<&str> = said.iter().map(|answer| answer.headline.as_str()).collect();
226            let headline = cut(&headlines.join(" "), 66);
227            let scale = fit(&headline, room, 6);
228            canvas.text(inner, at, &headline, scale, INK);
229            at += 8 * scale + 8;
230            // Its type and numbers, then its distribution: half of what Jev says.
231            let notes: Vec<String> = said.iter().filter_map(Answer::note).collect();
232            if !notes.is_empty() {
233                canvas.text(inner, at, &cut(&notes.join(" / "), room / 8), 1, INK);
234                at += 18;
235            }
236            if let [answer] = said {
237                detail(&mut canvas, inner, at, room, &answer.detail);
238            }
239        }
240        Card::Code { url, recurse, about, branch, latest, .. } => {
241            canvas.text(inner, top + 4, "Get the code", 4, INK);
242            // The command, white on black, as a terminal shows it.
243            let (box_y, box_h) = (top + 50, 44);
244            canvas.fill(inner, box_y, room, box_h, INK);
245            canvas.text(inner + 10, box_y + 10, if recurse { "$ git clone --recurse-submodules" } else { "$ git clone" }, 1, PAPER);
246            canvas.text(inner + 10, box_y + 26, &cut(&format!("  {url}"), (room - 20) / 8), 1, PAPER);
247            let mut at = box_y + box_h + 14;
248            if let Some(latest) = latest {
249                canvas.text(inner, at, &cut(&format!("{branch} @ {}", latest.line()), room / 8), 1, INK);
250                canvas.text(inner, at + 14, &cut(&latest.subject, room / 8), 1, INK);
251                at += 34;
252            }
253            canvas.text(inner, at, &cut(about, room / 8), 1, INK);
254        }
255    }
256    canvas
257}

A line with a square on it at p: the page's slider.

260fn slider(canvas: &mut Canvas, x: usize, y: usize, w: usize, p: f64) {
261    canvas.fill(x, y + 4, w, 1, INK);
262    let thumb = x + ((w - 9) as f64 * p.clamp(0.0, 1.0)).round() as usize;
263    canvas.fill(thumb, y, 9, 9, INK);
264}

An answer's numbers, drawn: where a yes-or-no fell between no and yes, the likeliest options of a pick, every level of a how-much.

268fn detail(canvas: &mut Canvas, x: usize, y: usize, room: usize, detail: &Detail) {
269    match detail {
270        Detail::Unknown => {}
271        Detail::Noul { p_yes: p } | Detail::Chance { p } => {
272            let (low, high) = if matches!(detail, Detail::Noul { .. }) { ("no", "yes") } else { ("0%", "100%") };
273            canvas.text(x, y, low, 1, INK);
274            canvas.text(x + room - width(high, 1), y, high, 1, INK);
275            slider(canvas, x, y + 12, room, *p);
276        }
277        Detail::Choice { options, .. } => {
278            for (row, (label, p)) in options.iter().take(3).enumerate() {
279                let at = y + row * 22;
280                let percent = format!("{:.0}%", p * 100.0);
281                canvas.text(x, at, &cut(label, (room - 48) / 8), 1, INK);
282                canvas.text(x + room - width(&percent, 1), at, &percent, 1, INK);
283                slider(canvas, x, at + 10, room, *p);
284            }
285        }
286        Detail::Score { score, levels, .. } => {
287            // One column a level, as tall as it is likely beside the
288            // likeliest; the nearest in ink.
289            let most = levels.iter().map(|(_, p)| *p).fold(0.0, f64::max).max(f64::EPSILON);
290            let count = levels.len().max(1);
291            let nearest = (score.round().max(0.0) as usize).min(count - 1);
292            let (gap, tall) = (6, 34);
293            let column = (room - gap * (count - 1)) / count;
294            for (level, (_, p)) in levels.iter().enumerate() {
295                let height = ((tall as f64) * (p / most).clamp(0.0, 1.0)).round() as usize;
296                let left = x + level * (column + gap);
297                canvas.fill(left, y + tall - height.max(1), column, height.max(1), if level == nearest { INK } else { GREY });
298                canvas.frame(left, y + tall - height.max(1), column, height.max(1), INK);
299            }
300            canvas.fill(x, y + tall, room, 1, INK);
301            if let (Some((lowest, _)), Some((highest, _))) = (levels.first(), levels.last()) {
302                let half = room / 16 - 1;
303                let highest = cut(highest, half);
304                canvas.text(x, y + tall + 5, &cut(lowest, half), 1, INK);
305                canvas.text(x + room - width(&highest, 1), y + tall + 5, &highest, 1, INK);
306            }
307        }
308    }
309}

The card as a PNG.

312pub fn png(card: Card<'_>) -> Vec<u8> {
313    let canvas = draw(card);
314    // Two bits a pixel, four pixels a byte, first pixel in the high bits.
315    let packed: Vec<u8> = canvas
316        .pixels
317        .chunks(4)
318        .map(|four| four.iter().enumerate().fold(0, |byte, (index, colour)| byte | colour << (6 - 2 * index)))
319        .collect();
320    let mut out = Vec::new();
321    let mut encoder = png::Encoder::new(&mut out, WIDTH as u32, HEIGHT as u32);
322    encoder.set_color(png::ColorType::Indexed);
323    encoder.set_depth(png::BitDepth::Two);
324    encoder.set_palette(PALETTE.to_vec());
325    encoder.set_compression(png::Compression::Fast);
326    let mut writer = encoder.write_header().expect("the header is valid for these fixed settings");
327    writer.write_image_data(&packed).expect("the data is the size the header says");
328    writer.finish().expect("writing to a Vec cannot fail");
329    out
330}
332#[cfg(test)]
333mod tests;