1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
|
// 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 the document that holds the topic today;
/// as topics move into docs/typ the value becomes the HTML page or chapter.
#let topics = (
tags: "docs/tags.md",
fs: "docs/fs.md",
config: "docs/config.md",
themes: "docs/themes.md",
keys: "docs/helix-keys.md",
detached: "docs/detached.md",
)
/// 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.
#let chapters = (
(id: "guide", title: "Guide", file: none),
(id: "cheatsheet", title: "Cheatsheet", file: "cheatsheet.typ"),
(id: "scripting", title: "Scripting", file: none),
(id: "reference", title: "Reference", file: none),
(id: "building", title: "Building", file: none),
)
/// The tutor, shown as it is in the editor (`Tutor`), from the repo root.
#let tutor-path = "/src/tutor.txt"
#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() {
html.elem("kbd", k.split(" ").map(t => html.elem("kbd", t)).join(" "))
} 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")
/// 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 },
))
})
}
/// 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", aria-label: label, title: label), (if shift { "⇧" } else { "" }) + b)
} 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 ")
html.elem("span", attrs: (class: "chord", aria-label: label, title: label), bs.pos().join("-"))
} 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"), {
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 {
block(
width: 100%, inset: (left: 3pt, y: 1.2pt), spacing: 0.4em,
stroke: (left: 1pt + palette.rule),
text(font: mono, size: 0.85em, fill: palette.ink, c),
)
}
/// Where the full story is: a topic id from `topics`, and a section in it.
#let doc(id, section: none) = {
let to = topics.at(id) + if section != none { "#" + section } else { "" }
context if html-out() { link(to)[→ #to] } else {
text(fill: palette.num)[→#m(to, fill: palette.num)]
}
}
/// 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)
let one(rows) = table(
columns: (auto, 1fr), stroke: none, inset: (x: 2pt, y: 1.6pt),
fill: (_, y) => if calc.even(y) { palette.tag-bg.transparentize(35%) },
..rows.flatten(),
)
context if html-out() or split == 1 { one(rows) } else {
let n = calc.ceil(rows.len() / split)
grid(columns: (1fr,) * split, column-gutter: 2pt, ..rows.chunks(n).map(one))
}
}
|