// The semantic elements every docs/typ topic uses, and the one place they // are defined. Content files call them ("this is a key", "this is a // builtin"); how each looks is decided here, per output: paged outputs // (the A4 cheatsheet, later the book) draw them, HTML export gives them // elements and classes for a stylesheet. Page geometry and fonts belong to // the output wrappers beside this file. /// The `atelier` theme (src/themes/atelier.zig): pardes's acme homage, /// butter paper and blue-green tags. #let palette = ( paper: rgb("#fffdeb"), ink: rgb("#293f35"), tag-bg: rgb("#e4f0ed"), tag-fg: rgb("#3d6055"), tag-name: rgb("#6e4774"), grip: rgb("#36826b"), col-grip: rgb("#955815"), str: rgb("#34743d"), num: rgb("#9a5810"), rule: rgb("#95baa2"), mouse: rgb("#8a3a00"), ) #let mono = "DejaVu Sans Mono" /// Topic ids for #doc: each names a chapter (an id in `chapters`) and, /// optionally, the label of a section in it. Old ids stay so older calls /// keep working. #let topics = ( guide: ("guide", none), setup: ("setup", none), cheatsheet: ("cheatsheet", none), scripting: ("scripting", none), reference: ("reference", none), themes: ("themes", none), building: ("building", none), tags: ("guide", none), fs: ("reference", none), config: ("guide", "config"), keys: ("guide", "keys"), detached: ("guide", "sessions"), ) /// Set by a wrapper that holds every chapter (the book, the HTML): #doc /// then links to the chapter or section. Alone (the A4 sheet) it names it. #let doc-links = state("doc-links", false) /// The book's and the HTML's reading order. `file` is the content file /// in docs/typ, or none while the topic is not written yet (the outputs /// then show a placeholder). Wrappers include each with heading offset 1, /// so a file's `=` headings sit under its chapter title, labelled /// `ch-`. #let chapters = ( (id: "guide", title: "Guide", file: "guide.typ"), (id: "setup", title: "Setting up your environment", file: "setup.typ"), (id: "cheatsheet", title: "Cheatsheet", file: "cheatsheet.typ"), (id: "scripting", title: "Scripting", file: "scripting.typ"), (id: "reference", title: "Reference", file: "reference.typ"), (id: "themes", title: "Themes", file: "themes.typ"), (id: "building", title: "Building, for contributors", file: "building.typ"), ) /// The one-line pitch: the A4 sheet's header and the site's landing page. #let tagline = "acme's tags and three buttons, helix's keys, terminals as panes, the editor as a 9P filesystem" /// The tutor, shown as it is in the editor (`Tutor`), from the repo root. #let tutor-path = "/src/tutor.txt" /// How much room tables and shell lines get: "roomy" (the book, the HTML) /// or "tight" (the A4 sheet sets it). #let density = state("density", "roomy") #let html-out() = target() == "html" #let span(class, body) = html.elem("span", attrs: (class: class), body) #let m(s, fill: palette.ink) = text(font: mono, size: 0.92em, fill: fill, s) /// A key: one press (`x`, `Ctrl-w`, `Shift-Esc`), or a sequence pressed one /// after another, written space-separated (`g g`, `SPC f c`). Either is ONE /// pill: a combination's parts joined by a muted +, a sequence's steps by a /// muted › (`Ctrl-w h` reads Ctrl + w › h). #let key(k) = context if html-out() { let sep(c, cls) = html.elem("span", attrs: (class: cls, "aria-hidden": "true"), c) let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(sep("+", "plus")) html.elem("kbd", attrs: (class: "key"), k.split(" ").map(press).join(sep("›", "then"))) } else { let muted(c) = text(fill: palette.grip.transparentize(25%), size: 0.85em, c) let press(t) = (if t.len() > 1 and t.contains("-") { t.split("-") } else { (t,) }).join(muted[+]) box( fill: palette.grip.transparentize(84%), radius: 2pt, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.5pt), text(font: mono, size: 0.92em, fill: palette.ink, k.split(" ").map(press).join(muted[#h(1pt)›#h(1pt)])), ) } /// Alternatives: separate keys listed together, each its own command. #let keys(..ks) = context ks.pos().map(key).join(if html-out() [ ] else { h(0.25em) }) #let button-names = ("B1": "left button", "B2": "wheel", "B3": "right button") #let click-names = ("B1": "left", "B2": "middle", "B3": "right") /// Text that is read and copied but not shown, beside a drawn glyph. #let hidden(t) = html.elem("span", attrs: (class: "vh"), t) /// A mouse, about cap height: left button, wheel, right button. The parts /// in `pressed` ("B1", "B2", "B3") are filled. #let mouse(pressed) = { let (w, h, top) = (0.84em, 1.2em, 0.56em) let ink = palette.mouse let s = 0.7pt + ink let on(b) = if b in pressed { ink } else { none } box(width: w, height: h, baseline: 0.24em, { place(rect(width: w / 2, height: top, radius: (top-left: w / 2), fill: on("B1"), stroke: s)) place(dx: w / 2, rect(width: w / 2, height: top, radius: (top-right: w / 2), fill: on("B3"), stroke: s)) place(dy: top, rect(width: w, height: h - top, radius: (bottom: w / 2), stroke: s)) // the wheel: a narrow tall slot over the buttons' seam, on a paper // halo so a filled wheel stands apart from the button outlines let (ww, wh, halo) = (0.26em, 0.44em, 0.07em) let (wx, wy) = ((w - ww) / 2, 0.06em) place(dx: wx - halo, dy: wy - halo / 2, rect( width: ww + 2 * halo, height: wh + halo, radius: ww, fill: palette.paper)) place(dx: wx, dy: wy, rect( width: ww, height: wh, radius: ww / 2, stroke: 0.5pt + ink, fill: if "B2" in pressed { ink } else { palette.paper }, )) }) } /// The same mouse as inline SVG for HTML. Each part is classed `on` /// (pressed) or `off`, and the stylesheet colours them: the pressed part /// filled in the accent, the rest outlined and muted. #let mouse-svg(pressed) = { let cls(b) = if b in pressed { "on" } else { "off" } let part(d, b) = html.elem("path", attrs: (class: cls(b), d: d)) html.elem("svg", attrs: (class: "mouse", viewBox: "-6 -6 96 132", "aria-hidden": "true"), { // taller buttons and a slimmer wheel than on paper, so a pressed // button shows as a solid block at body size part("M0 64H84V78A42 42 0 0 1 0 78Z", none) part("M0 64V42A42 42 0 0 1 42 0V64Z", "B1") part("M42 0A42 42 0 0 1 84 42V64H42Z", "B3") // a pressed wheel is drawn wider, so it reads as pressed, not as a seam let (hx, hw, wx, ww) = if "B2" in pressed { ("27", "30", "31", "22") } else { ("31", "22", "35", "14") } html.elem("rect", attrs: (class: "halo", x: hx, y: "4", width: hw, height: "48", rx: "13")) html.elem("rect", attrs: (class: "wheel " + cls("B2"), x: wx, y: "8", width: ww, height: "40", rx: "9")) }) } /// A mouse button; `shift: true` for the button with Shift held. #let btn(b, shift: false) = context if html-out() { let label = (if shift { "shift " } else { "" }) + button-names.at(b) html.elem("span", attrs: (class: "btn", title: label), { if shift { html.elem("span", attrs: (class: "shift", "aria-hidden": "true"), "⇧") } mouse-svg((b,)) hidden((if shift { "Shift-" } else { "" }) + click-names.at(b) + "-click") }) } else { if shift { text(size: 0.8em, fill: palette.col-grip)[⇧] } mouse((b,)) } /// A chord: the first button held, each next one clicked while it is. /// Drawn as the held button, then for each click the held one and that one /// down, with arrows between. #let chord(..bs) = context if html-out() { let label = bs.pos().map(b => button-names.at(b)).join(" then ") let (held, ..clicks) = bs.pos() let arrow = html.elem("span", attrs: (class: "then", "aria-hidden": "true"), "›") let said = "hold " + click-names.at(held) + ", click " + clicks.map(c => click-names.at(c)).join(", then ") html.elem("span", attrs: (class: "chord", title: label), { ((mouse-svg((held,)),) + clicks.map(c => mouse-svg((held, c)))).join(arrow) hidden(said) }) } else { let (held, ..clicks) = bs.pos() let arrow = text(size: 0.75em, fill: palette.col-grip)[#h(0.5pt)›#h(0.5pt)] ((mouse((held,)),) + clicks.map(c => mouse((held, c)))).join(arrow) } /// A tag word or builtin, as it sits in a tag. #let word(w) = context if html-out() { span("word", raw(w)) } else { box(fill: palette.tag-bg, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.4pt), m(w, fill: palette.tag-fg)) } /// A whole tagline: its grip, the path, then its words. #let tag(words, path: none) = context if html-out() { html.elem("div", attrs: (class: "tag"), { html.elem("span", attrs: (class: "grip", "aria-hidden": "true"))[] if path != none { span("path", path); [ ] } raw(words) }) } else { block(width: 100%, fill: palette.tag-bg, inset: (x: 2pt, y: 2.4pt), spacing: 0.5em, { box(fill: palette.grip, width: 0.55em, height: 0.75em, baseline: 0.1em) h(3pt) if path != none { m(path, fill: palette.tag-name); h(0.6em) } m(words, fill: palette.tag-fg) }) } /// A look address. #let addr(a) = context if html-out() { span("addr", raw(a)) } else { m(a, fill: palette.tag-name) } /// A 9P path. #let file(p) = context if html-out() { span("file", raw(p)) } else { m(p, fill: palette.str) } /// A shell line. #let cmd(c) = context if html-out() { raw(c, block: true, lang: "sh") } else { let tight = density.get() == "tight" block( width: 100%, spacing: if tight { 0.4em } else { 0.9em }, inset: if tight { (left: 3pt, y: 1.2pt) } else { (left: 6pt, y: 3pt) }, stroke: (left: (if tight { 1pt } else { 1.5pt }) + palette.rule), fill: if tight { none } else { palette.tag-bg.transparentize(50%) }, text(font: mono, size: 0.85em, fill: palette.ink, c), ) } /// Where the full story is: a topic id from `topics`, and a section label /// in it. A link where the wrapper holds the chapters, else its name. #let doc(id, section: none) = { let (chapter, default) = topics.at(id) let section = if section != none { section } else { default } let title = chapters.find(c => c.id == chapter).title let to = label(if section != none { section } else { "ch-" + chapter }) let named = title + if section != none { ": " + section.replace("-", " ") } else { "" } context { if doc-links.get() { let found = query(to) let shown = if found.len() > 0 and found.first().func() == heading { if section != none [#title: #found.first().body] else [#title] } else [#named] link(to, text(fill: palette.num)[→ #shown]) } else { text(fill: palette.num)[→ #named] } } } /// Pairs: a thing and what it means, as a two-column table, key column /// first, rows on alternating tints. `split: 2` sets the rows as that many /// tables side by side (on paper; HTML gets one table). #let pairs(split: 1, ..cells) = { let rows = cells.pos().chunks(2) context { let tight = density.get() == "tight" let one(rows) = table( columns: (auto, 1fr), stroke: none, inset: if tight { (x: 2pt, y: 1.6pt) } else { (x: 7pt, y: 4pt) }, fill: (_, y) => if calc.even(y) { palette.tag-bg.transparentize(35%) }, ..rows.flatten(), ) if html-out() or split == 1 { one(rows) } else { let n = calc.ceil(rows.len() / split) grid(columns: (1fr,) * split, column-gutter: if tight { 2pt } else { 10pt }, ..rows.chunks(n).map(one)) } } } /// Where pardes lives and under what licence, for the site's footer (and /// any output that wants them). none until decided. #let project = (source: none, license: none) /// A glossary: like #pairs, under its own heading. On paper the heading /// and the table; in HTML a
that starts closed, its summary the /// heading and the count, so a reader who knows the words skips them. #let glossary(title, lbl, ..cells) = context if html-out() { html.elem("details", attrs: (class: "glossary"), { html.elem("summary", [#heading(title) #lbl #html.elem("span", attrs: (class: "count"), "(" + str(calc.quo(cells.pos().len(), 2)) + ")")]) pairs(..cells) }) } else { [#heading(title) #lbl] pairs(..cells) }