diff options
| author | Gabriel Schneider <[email protected]> | 2026-10-01 13:05:10 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 13:16:47 -0300 |
| commit | f700b2af8e874c586e6996b8311c3983203c6637 (patch) | |
| tree | 89c7de940f888a0917e5851c8a362c83de50b389 /docs/typ/style.typ | |
| parent | f6a511c61d7ef2ff680f70077b236559a595be69 (diff) | |
| download | pardes-f700b2af8e874c586e6996b8311c3983203c6637.tar.gz pardes-f700b2af8e874c586e6996b8311c3983203c6637.zip | |
Builtins are chips that link to a glossary generated from the registry, and the virtual filesystem is drawn as a tree
#word draws a builtin as a chip on the tag's ground over a rule (Look and
Exec in acme's look green and exec red), a <code class="word"> in HTML,
and links it to its entry in a new Builtins chapter wherever that
chapter is in the same output (the book, the site). The chapter is
drawn from docs/typ/builtins-tty.json and builtins-gui.json, which
`zig build builtins-json -Dplatform=tty|gui` writes from the registry
and the Help rows (src/builtins_json.zig, tools/gen_builtins_json.zig;
summaryOf in ninep/ctl.zig is now pub); style.typ merges them, marking
the window-only words. The doc steps refresh their build's file first,
and each build's unit-test fails when its file is stale. Everyday words
first (`everyday` in style.typ), then groups by leader prefix as Help
has them, then settings, the words with no path, debugging and
internals, and last an A-Z index of every word. `zig build book` joins
the doc steps.
#fstree draws a directory tree with its connectors, a description
column and links to the sections that document each file; reference.typ
draws the served tree with it, each pane, pty and column file on its
own line, and a test in config.zig checks it against ninep/tree.zig's
files both ways. The cheatsheet has a two-level version. On the site
the tree reads as `tree` text in w3m and wraps descriptions under the
names on a phone.
The site's tutor draws its B1, B2, B3 and 1-2 chords as mouse glyphs;
the landing credits Plan 9 and acme, as does the book's title page;
install.typ no longer lists the mirrors (the footer does).
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'docs/typ/style.typ')
| -rw-r--r-- | docs/typ/style.typ | 221 |
1 files changed, 218 insertions, 3 deletions
diff --git a/docs/typ/style.typ b/docs/typ/style.typ index 065cc210..2f63f3b2 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. @@ -299,3 +344,173 @@ }) }) } + +/// 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())) + } +} |
