// jj-log.typ — Typst document that renders jj log JSON output // // Usage: // jj log -r '::@' --no-graph \ // -T '{"commit": ' ++ json(self) ++ ', "bookmarks": ' ++ json(local_bookmarks) ++ ', "empty": ' ++ if(empty, "true", "false") ++ ', "conflict": ' ++ if(conflict, "true", "false") ++ ', "root": ' ++ if(root, "true", "false") ++ '}\n' \ // > log.json // typst compile jj-log.typ --input data=log.json #let data-path = sys.inputs.at("data", default: "log.json") #let raw-text = read(data-path) #let lines = raw-text.split("\n").filter(l => l.trim() != "") #let commits = lines.map(l => json(bytes(l))) // Filter out root commit #let commits = commits.filter(c => not c.root) // --- Helpers --- #let mono(body) = text(font: "DejaVu Sans Mono", size: 0.85em, body) #let short(hash) = hash.slice(0, 8) #let tag-pill(name, color: rgb("#4a6fa5")) = { box( fill: color.lighten(75%), stroke: 0.5pt + color, radius: 3pt, inset: (x: 4pt, y: 2pt), text(size: 0.8em, font: "DejaVu Sans Mono", fill: color, weight: "bold", name), ) } #let format-date(ts) = { // timestamp is ISO 8601, e.g. "2026-03-27T14:22:35-03:00" // extract date + time portion let date-part = ts.slice(0, 10) let time-part = ts.slice(11, 16) date-part + " " + time-part } // Collect all bookmarks across commits #let all-bookmarks = commits.map(c => { c.bookmarks.map(b => (name: b.name, commit: short(c.commit.commit_id), change: short(c.commit.change_id), desc: c.commit.description.trim())) }).flatten() // --- Page setup --- #set page(paper: "a4", margin: 2cm) #set text(font: "DejaVu Sans", size: 10pt) #set par(leading: 0.6em) // --- Title --- #align(center)[ #text(size: 18pt, weight: "bold")[jj ancestor log] #v(0.3em) #text(size: 9pt, fill: gray)[ #commits.len() commits #h(1em) #all-bookmarks.len() bookmarks #h(1em) newest → oldest ] ] #v(1em) // --- Bookmarks index --- #if all-bookmarks.len() > 0 [ == Bookmarks #for bm in all-bookmarks [ - #tag-pill(bm.name) #h(0.5em) at #mono(bm.commit) — #emph(bm.desc) ] #v(1em) ] // --- Commit log table --- == Commit History #table( columns: (auto, auto, 1fr, auto), align: (center, center, left, right), stroke: 0.4pt + luma(200), inset: 6pt, // Header table.header( text(weight: "bold")[Change], text(weight: "bold")[Commit], text(weight: "bold")[Description], text(weight: "bold")[Date], ), // Rows — newest to oldest (jj log default order with ::@) ..commits.map(c => { let bookmarks = c.bookmarks.map(b => tag-pill(b.name)) let is-empty = c.empty let has-conflict = c.conflict let desc = c.commit.description.trim() if desc == "" { desc = "(no description)" } let badges = { if is-empty { tag-pill("empty", color: rgb("#999")) } if has-conflict { tag-pill("conflict", color: rgb("#c44")) } } ( mono(short(c.commit.change_id)), mono(short(c.commit.commit_id)), [ #desc #if bookmarks.len() > 0 { h(0.5em); bookmarks.join(h(0.3em)) } #badges ], text(size: 0.85em, fill: luma(100), format-date(c.commit.author.timestamp)), ) }).flatten(), ) // --- Detailed list --- #v(1.5em) == Commit Details #for (i, c) in commits.enumerate() [ #let desc = c.commit.description.trim() #if desc == "" { desc = "(no description)" } #let bookmarks = c.bookmarks.map(b => tag-pill(b.name)) #block( width: 100%, inset: 8pt, radius: 4pt, stroke: 0.3pt + luma(200), fill: if calc.even(i) { luma(252) } else { white }, )[ #grid( columns: (auto, 1fr), column-gutter: 1em, row-gutter: 0.4em, text(fill: luma(120), size: 0.85em)[change:], mono(short(c.commit.change_id)), text(fill: luma(120), size: 0.85em)[commit:], mono(short(c.commit.commit_id)), text(fill: luma(120), size: 0.85em)[author:], [#c.commit.author.name #text(fill: luma(120))[\<#c.commit.author.email\>]], text(fill: luma(120), size: 0.85em)[date:], format-date(c.commit.author.timestamp), ..if bookmarks.len() > 0 {( text(fill: luma(120), size: 0.85em)[bookmarks:], bookmarks.join(h(0.3em)), )}, ) #v(0.3em) #text(weight: "bold", size: 1.05em, desc) #if c.empty [ #text(fill: luma(150), size: 0.85em)[\[empty\]] ] #if c.conflict [ #text(fill: rgb("#c44"), size: 0.85em)[\[conflict\]] ] ] #v(0.5em) ]