summaryrefslogtreecommitdiff
path: root/docs/typ/style.typ
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-10-01 13:05:10 -0300
committerGabriel Schneider <[email protected]>2026-10-01 13:16:47 -0300
commitf700b2af8e874c586e6996b8311c3983203c6637 (patch)
tree89c7de940f888a0917e5851c8a362c83de50b389 /docs/typ/style.typ
parentf6a511c61d7ef2ff680f70077b236559a595be69 (diff)
downloadpardes-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.typ221
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()))
+ }
+}