summaryrefslogtreecommitdiff
path: root/docs/typ/style.typ
diff options
context:
space:
mode:
Diffstat (limited to 'docs/typ/style.typ')
-rw-r--r--docs/typ/style.typ323
1 files changed, 318 insertions, 5 deletions
diff --git a/docs/typ/style.typ b/docs/typ/style.typ
index 065cc210..b5c2ff85 100644
--- a/docs/typ/style.typ
+++ b/docs/typ/style.typ
@@ -33,6 +33,7 @@
reference: ("reference", none),
themes: ("themes", none),
building: ("building", none),
+ builtins: ("builtins", none),
tags: ("guide", none),
fs: ("reference", none),
config: ("guide", "config"),
@@ -55,6 +56,7 @@
(id: "cheatsheet", title: "Cheatsheet", file: "cheatsheet.typ"),
(id: "scripting", title: "Scripting", file: "scripting.typ"),
(id: "reference", title: "Reference", file: "reference.typ"),
+ (id: "builtins", title: "Builtins", file: "builtins.typ"),
(id: "themes", title: "Themes", file: "themes.typ"),
(id: "building", title: "Building, for contributors", file: "building.typ"),
)
@@ -174,9 +176,52 @@
((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 tag word or builtin, drawn as a thing to click: the tag's ground and
+/// ink in the tag's monospace, over a rule. Look and Exec take acme's look
+/// and exec sweep colours (green and red, src/themes/acme.zig) for the
+/// rule; every other word the tag's box colour. In HTML a <code
+/// class="word">, so a text browser still shows the plain word.
+#let word-accent = (Look: rgb("#006600"), Exec: rgb("#aa0000"))
+
+/// Every builtin of either build, written by `zig build builtins-json`
+/// from the registry and the Help rows (src/builtins_json.zig), a file per
+/// build: name, leader, also (Help's other shortcuts), arg, scope, setting,
+/// choices, doc. Merged here, each with `platforms`: "both", "tty" (the
+/// terminal only) or "gui" (the window only).
+#let builtin-data = {
+ let tty = json("builtins-tty.json")
+ let gui = json("builtins-gui.json")
+ let in-tty = tty.map(b => b.name)
+ let in-gui = gui.map(b => b.name)
+ let all = (tty + gui.filter(b => b.name not in in-tty)).sorted(key: b => b.name)
+ all.map(b => {
+ b.insert("platforms", if b.name in in-tty and b.name in in-gui { "both" } else if b.name in in-tty { "tty" } else { "gui" })
+ b
+ })
+}
+#let builtin-names = builtin-data.map(b => b.name)
+
+#let word-chip(w) = context if html-out() {
+ let base = w.split("+").first()
+ let kind = if base == "Look" { " look" } else if base == "Exec" { " exec" } else { "" }
+ html.elem("code", attrs: (class: "word" + kind), w)
+} else {
+ let rule = word-accent.at(w.split("+").first(), default: palette.grip)
+ box(
+ fill: palette.tag-bg, inset: (x: 1.4pt, y: 0pt), outset: (y: 1.4pt),
+ stroke: (bottom: 0.6pt + rule),
+ m(w, fill: palette.tag-fg),
+ )
+}
+
+/// A builtin's word links to its glossary entry wherever the glossary is
+/// part of the same output (the book, the site); elsewhere it is the chip.
+#let word(w) = context {
+ let base = w.split("+").first()
+ let target = label("builtin-" + base)
+ if doc-links.get() and base in builtin-names and query(target).len() > 0 {
+ link(target, word-chip(w))
+ } else { word-chip(w) }
}
/// A whole tagline: its grip, the path, then its words.
@@ -263,15 +308,34 @@
(name: "git.0x4200.cafe", web: "https://git.0x4200.cafe/pardes", clone: "https://git.0x4200.cafe/pardes.git"),
),
license: "ISC",
+ changes: "https://git.sr.ht/~gbrls/pardes/tree/main/item/src/CHANGELOG.md",
+ // built binaries to download: the version they are, where the
+ // checksums are, and a file per program (install.typ shows them)
+ download: (
+ version: "0.23",
+ sums: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/SHA256SUMS",
+ files: (
+ (program: "pardes", what: "the terminal build, static, any x86_64 Linux",
+ url: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/pardes-x86_64-linux-static", bytes: 62223912,
+ sha256: "740992b18bdcfefc4db449dcbdb12423b4505eb2b3fb360aaa6dc57cc2fdda9f"),
+ (program: "pardes-gui", what: "the window, x86_64 Linux with glibc 2.29 or later; it loads Wayland or X11 and GL or Vulkan from the system",
+ url: "https://pub-66384ba880784f119ce95123b92fa5c6.r2.dev/pardes-gui-x86_64-linux", bytes: 66786824,
+ sha256: "a1c68b347d424b8077fb5af4050dd8455dc849adb0c1e2fc325b1646f5a255e5"),
+ ),
+ ),
)
/// A glossary: like #pairs, under its own heading. On paper the heading
/// and the table; in HTML a <details> 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() {
+ // a real definition list, so a text browser reads term, then meaning
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)
+ html.elem("summary", [#heading(title) #lbl])
+ html.elem("dl", for (term, meaning) in cells.pos().chunks(2) {
+ html.elem("dt", term)
+ html.elem("dd", meaning)
+ })
})
} else {
[#heading(title) #lbl]
@@ -299,3 +363,252 @@
})
})
}
+
+/// The newcomer's words, first in the glossary; every other builtin goes
+/// under its group below. Each must be in builtins.json.
+#let everyday = ("Look", "Exec", "Save", "Undo", "Redo", "New", "Newcol", "Del", "Delcol",
+ "Tty", "Kill", "Find", "Grep", "Recent", "Dump", "Restore", "Exit", "Help", "Tutor", "Themes", "Config")
+/// Debugging and developer words, last whatever their leader path.
+#let internals = ("Debug", "EffectCode", "Lspwhy", "Messages", "DumpConfig", "DumpThemes")
+
+/// The glossary's groups, in order: by the leader path's first key, the
+/// way Help groups them, then settings, the words with no path, internals.
+#let builtin-groups = (
+ (title: "Files", keys: ("f",)),
+ (title: "Panes and columns", keys: ("c", "d")),
+ (title: "Moving and jumps", keys: ("w", "j")),
+ (title: "Terminals", keys: ("n",)),
+ (title: "Language server", keys: ("l",)),
+ (title: "Sessions", keys: ("s", "q")),
+ (title: "Help", keys: ("h", "?")),
+ (title: "Clipboard", keys: ("y", "Y", "p", "P", "R")),
+ (title: "Toggles", keys: ("t",)),
+ (title: "Settings and themes", keys: "setting"),
+ (title: "Animations", keys: ("a",)),
+ (title: "Other words", keys: "none"),
+)
+
+#let group-of(b) = {
+ if b.name in internals { return "internals" }
+ if b.setting { return "setting" }
+ if b.leader == none { return "none" }
+ b.leader.split(" ").at(1)
+}
+
+/// Help spells a shortcut its own way (C-w left, right-click, topbar);
+/// this draws it as the docs do.
+#let shortcut(s) = {
+ if s == "topbar" { return [in the workspace tag] }
+ if s == "middle-click" { return btn("B2") }
+ if s == "right-click" { return btn("B3") }
+ if s == "left-click" { return btn("B1") }
+ let names = (enter: "Enter", tab: "Tab", escape: "Esc", left: "Left", right: "Right", up: "Up", down: "Down",
+ home: "Home", end: "End", backspace: "Backspace", delete: "Delete", page_up: "PgUp", page_down: "PgDn")
+ key(s.split(" ").map(t => {
+ let mods = ""
+ while t.len() > 2 and t.at(1) == "-" and t.at(0) in ("C", "A", "S") {
+ mods += (C: "Ctrl-", A: "Alt-", S: "Shift-").at(t.at(0))
+ t = t.slice(2)
+ }
+ mods + names.at(t, default: t)
+ }).join(" "))
+}
+
+/// A doc comment's text, its `code` spans set as code.
+#let prose(t) = t.split("`").enumerate().map(((i, part)) => if calc.odd(i) { raw(part) } else { part }).join()
+
+#let builtin-entry(b) = [
+ #heading(depth: 2, raw(b.name)) #label("builtin-" + b.name)
+ #let keys = (if b.leader != none { (key(b.leader),) } else { () }) + b.also.map(shortcut)
+ #if b.platforms != "both" [#emph(if b.platforms == "gui" [window only] else [terminal only]) · ]
+ #if keys.len() > 0 [#keys.join([, ]) · ]
+ #if b.choices != none [#raw(b.choices.replace(", ", "|")) · ] else if b.arg [takes an argument · ]
+ #prose(b.doc)
+]
+
+/// The glossary: everyday words first, then the groups, then internals,
+/// each alphabetical. A level-1 heading per group in the content file.
+#let builtin-glossary() = {
+ let by-name = (:)
+ for b in builtin-data { by-name.insert(b.name, b) }
+ [= Everyday <builtins-everyday>]
+ for n in everyday.sorted() { builtin-entry(by-name.at(n)) }
+ let rest = builtin-data.filter(b => b.name not in everyday)
+ for g in builtin-groups {
+ let members = rest.filter(b => {
+ let k = group-of(b)
+ if type(g.keys) == str { k == g.keys } else { k in g.keys }
+ })
+ if members.len() > 0 {
+ heading(depth: 1, g.title)
+ for b in members { builtin-entry(b) }
+ }
+ }
+ let inner = rest.filter(b => group-of(b) == "internals")
+ if inner.len() > 0 {
+ [= Debugging and internals]
+ for b in inner { builtin-entry(b) }
+ }
+ // a builtin no group took is a bug in builtin-groups, not a silent drop
+ let placed = everyday + rest.filter(b => {
+ let k = group-of(b)
+ k == "internals" or builtin-groups.any(g => if type(g.keys) == str { k == g.keys } else { k in g.keys })
+ }).map(b => b.name)
+ for b in builtin-data { assert(b.name in placed, message: "builtin " + b.name + " is in no glossary group") }
+ // last, every word at once, A to Z, each a link to its entry
+ [= All builtins, A–Z <builtins-a-z>]
+ context if html-out() {
+ html.elem("p", attrs: (class: "all-words"),
+ builtin-data.map(b => link(label("builtin-" + b.name), word-chip(b.name))).join(" "))
+ } else {
+ par(builtin-data.map(b => link(label("builtin-" + b.name), word-chip(b.name))).join(h(0.4em)))
+ }
+}
+
+/// A directory tree drawn as `tree` draws one. `spec` is a raw block, a
+/// line per file: two spaces of indent per level, the name (a directory
+/// ends in `/`), two or more spaces, a one-line description, and an
+/// optional `@label` naming the section that documents it, which becomes
+/// a link wherever that section is in the same output. A description may
+/// call #word, #key, #keys or #btn. src/config.zig's
+/// test reads the same block in reference.typ against the served tree.
+#let fstree(spec, root: "/") = {
+ // a description is text, unless it calls #word, #key or #btn
+ let said(d) = if d.contains("#") { eval(d, mode: "markup", scope: (word: word, key: key, keys: keys, btn: btn)) } else { d }
+ let rows = spec.text.split("\n").filter(l => l.trim() != "").map(l => {
+ let depth = calc.quo(l.len() - l.trim(at: start).len(), 2)
+ let m = l.trim().match(regex("^(\S+)(?:\s{2,}(.*?))?(?:\s+@([a-z0-9-]+))?$"))
+ (depth: depth, name: m.captures.at(0), desc: m.captures.at(1), anchor: m.captures.at(2))
+ })
+ // the connector for each row: is each ancestor, and the row itself, the
+ // last among its siblings?
+ let last(i) = {
+ let d = rows.at(i).depth
+ for j in range(i + 1, rows.len()) {
+ if rows.at(j).depth < d { return true }
+ if rows.at(j).depth == d { return false }
+ }
+ true
+ }
+ let lasts = range(rows.len()).map(last)
+ let prefix(i) = {
+ let d = rows.at(i).depth
+ let out = ""
+ // for each ancestor depth, the nearest row above at that depth
+ for k in range(d) {
+ let a = range(i).rev().find(j => rows.at(j).depth == k)
+ out += if lasts.at(a) { " " } else { "│ " }
+ }
+ out + if lasts.at(i) { "└── " } else { "├── " }
+ }
+ let named(r) = context {
+ let t = if r.anchor != none { label(r.anchor) } else { none }
+ if t != none and query(t).len() > 0 { link(t, r.name) } else { r.name }
+ }
+ let width = calc.max(..range(rows.len()).map(i => prefix(i).len() + rows.at(i).name.len()))
+ context if html-out() {
+ let nl = html.elem("span", attrs: (class: "nl"), "\n")
+ html.elem("pre", attrs: (class: "fstree", style: "--name:" + str(width + 2) + "ch"), {
+ html.elem("span", attrs: (class: "row root"), [#html.elem("span", attrs: (class: "name dir"), root)#nl])
+ for (i, r) in rows.enumerate() {
+ let pad = " " * (width - prefix(i).clusters().len() - r.name.len() + 2)
+ html.elem("span", attrs: (class: "row"), {
+ html.elem("span", attrs: (class: if r.name.ends-with("/") { "name dir" } else { "name" }),
+ [#html.elem("span", attrs: (class: "branch"), prefix(i))#named(r)#pad])
+ if r.desc != none {
+ html.elem("span", attrs: (class: "desc", style: "--cols:" + str(prefix(i).clusters().len())), said(r.desc))
+ }
+ nl
+ })
+ }
+ })
+ } else {
+ set text(font: mono, size: 0.82em)
+ block(width: 100%, grid(columns: (auto, 1fr), column-gutter: 1.2em, row-gutter: 0pt, inset: (y: 0.12em),
+ text(weight: "bold", fill: palette.grip, root), [],
+ ..rows.enumerate().map(((i, r)) => (
+ [#text(fill: palette.rule, prefix(i))#text(fill: if r.name.ends-with("/") { palette.grip } else { palette.ink },
+ weight: if r.name.ends-with("/") { "bold" } else { "regular" }, named(r))],
+ text(font: "Libertinus Serif", size: 1.2em, fill: palette.tag-fg, if r.desc == none { [] } else { said(r.desc) }),
+ )).flatten()))
+ }
+}
+
+/// A screenshot with numbered pins on it and their labels as text beside
+/// it, so the labels read, search and translate as text. `src` is from
+/// the repository root (the site serves it from the same path under
+/// docs/site/); `size` is its pixel (width, height); each pin is (label,
+/// x, y) in pixels of that image.
+#let annotated(src, size, alt: "", ..pins) = {
+ let (w, h) = size
+ let pins = pins.pos()
+ context if html-out() {
+ let web = src.trim("/docs/site/", at: start)
+ html.elem("figure", attrs: (class: "annotated"), {
+ html.elem("div", attrs: (class: "frame"), {
+ html.elem("img", attrs: (src: web, alt: alt, width: str(w), height: str(h)))
+ for (i, (label, x, y)) in pins.enumerate() {
+ html.elem("span", attrs: (class: "pin", "aria-hidden": "true",
+ // repr, not str: str writes a negative with U+2212, which CSS rejects
+ style: "left:" + repr(calc.round(100 * x / w, digits: 2)) + "%;top:" + repr(calc.round(100 * y / h, digits: 2)) + "%"), str(i + 1))
+ }
+ })
+ html.elem("ol", attrs: (class: "pins"), for (label, x, y) in pins { html.elem("li", label) })
+ })
+ } else {
+ let width = 11cm
+ let k = width / w
+ let pin(n) = box(width: 1.25em, height: 1.25em, radius: 50%, fill: palette.mouse,
+ align(center + horizon, text(size: 0.75em, weight: "bold", fill: white, str(n))))
+ block(breakable: false, grid(columns: (width, 1fr), column-gutter: 1.2em, align: horizon,
+ box(width: width, height: h * k, {
+ place(top + left, image(src, width: width, alt: alt))
+ for (i, (label, x, y)) in pins.enumerate() {
+ place(top + left, dx: x * k - 0.625em, dy: y * k - 0.625em, pin(i + 1))
+ }
+ }),
+ enum(..pins.map(p => p.at(0)), spacing: 0.7em),
+ ))
+ }
+}
+
+/// A pane's box as it looks: blank in normal mode, `^` in insert, `$` raw
+/// (a terminal's own input); `unsaved: true` fills it, as an unsaved
+/// file's is. In HTML a span the stylesheet draws, its mode said in words.
+#let mode-box(mark, unsaved: false) = {
+ let mode = if mark == "^" { "insert" } else if mark == "$" { "raw" } else { "normal" }
+ context if html-out() {
+ html.elem("span", attrs: (class: "mode-box" + if unsaved { " unsaved" } else { "" },
+ role: "img", "aria-label": mode + " mode box" + if unsaved { ", unsaved" } else { "" }), mark)
+ } else {
+ box(width: 1.05em, height: 1.05em, baseline: 0.15em, radius: 1pt,
+ fill: if unsaved { palette.col-grip } else { palette.tag-bg },
+ stroke: 0.7pt + palette.grip,
+ align(center + horizon, text(font: mono, size: 0.8em, weight: "bold",
+ fill: if unsaved { white } else { palette.ink }, mark)))
+ }
+}
+
+/// A recording from docs/site/media/clips.toml, by name. On the site the
+/// same video as the landing's (poster, autoplay muted loop, played while
+/// in view) and its caption; in print its poster, small, and the caption.
+/// `wide: true` lets it take the page's full width (a recording of many
+/// narrow columns).
+#let clip(name, wide: false) = {
+ let c = toml("../site/media/clips.toml").clip.find(c => c.name == name)
+ assert(c != none, message: "no clip named " + name + " in clips.toml")
+ let said = if c.caption.contains("#") { eval(c.caption, mode: "markup") } else { c.caption }
+ context if html-out() {
+ html.elem("figure", attrs: (class: "clip inline" + if wide { " wide" } else { "" }, id: "rec-" + c.name), {
+ html.elem("video", attrs: (poster: "media/" + c.name + ".png", autoplay: "", muted: "", loop: "",
+ playsinline: "", controls: ""), {
+ html.elem("source", attrs: (src: "media/" + c.name + ".webm", type: "video/webm"))
+ html.elem("source", attrs: (src: "media/" + c.name + ".mp4", type: "video/mp4"))
+ })
+ html.elem("figcaption", said)
+ })
+ } else {
+ figure(image("/docs/site/media/" + c.name + ".png", width: if wide { 11cm } else { 7cm }, alt: c.caption),
+ caption: text(size: 0.9em, said), numbering: none, kind: "clip", supplement: none)
+ }
+}