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
|
// css.go — the stylesheet is generated per site: theme colors become custom
// properties, one block per scheme, and the per-agent coverage colors are
// blended from them in the browser so any theme can supply them.
//
// Everything switchable is a custom property on :root. That is what lets
// themes.css hold nothing but one block per theme (writeThemesCSS) and the
// picker repaint the page by setting a single attribute.
package web
import (
"encoding/json"
"fmt"
"io"
"strings"
texttemplate "text/template"
)
type cssVar struct{ K, V string }
type synCSS struct{ Class string }
type maskCSS struct {
Mask uint8
Mix string
}
func renderCSS(w io.Writer, site *Site) error {
css, err := texttemplate.ParseFS(tfs, "assets/style.css.tmpl")
if err != nil {
return err
}
keys := syntaxKeys(site.Theme, site.Light)
var syn []synCSS
for _, k := range keys {
syn = append(syn, synCSS{Class: cssKey(k)})
}
var masks []maskCSS
for _, m := range site.masks() {
masks = append(masks, maskCSS{Mask: m, Mix: site.maskMix(m)})
}
return css.Execute(w, map[string]any{
"DarkName": site.Theme.Name, "LightName": site.Light.Name,
"DarkVars": themeVars(site.Theme, keys), "LightVars": themeVars(site.Light, keys),
"Syntax": syn, "Masks": masks,
})
}
// themeVars is one theme as custom properties: its own colors, the syntax
// colors under the keys the site paints with (resolved along their dots, so a
// theme that only names "punctuation" still colors brackets), and the agent
// colors every coverage blend is mixed from.
func themeVars(t *Theme, keys []string) []cssVar {
// bg and panel are what everything else is read against, so they are the
// two that may not carry a theme's alpha channel through
vars := []cssVar{
{"--bg", hex6(t.Bg)}, {"--fg", t.Fg}, {"--panel", hex6(t.Panel)}, {"--border", t.Border},
{"--ln", t.LineNo}, {"--muted", t.Muted}, {"--accent", t.Accent},
{"--note", t.Note}, {"--hover", t.Hover},
}
for _, k := range keys {
key := resolveKey(t, k)
if key == "" {
continue
}
s, c := t.Syntax[key], cssKey(k)
vars = append(vars, cssVar{"--s-" + c, s.Color})
if s.FontStyle != nil && *s.FontStyle != "" && *s.FontStyle != "normal" {
vars = append(vars, cssVar{"--s-" + c + "-fs", *s.FontStyle})
}
if s.FontWeight != nil && *s.FontWeight > 0 {
vars = append(vars, cssVar{"--s-" + c + "-fw", fmt.Sprintf("%.0f", *s.FontWeight)})
}
}
for i := 0; i < maxAgents; i++ {
vars = append(vars, cssVar{fmt.Sprintf("--ag%d", i), hex6(t.Player(i))})
}
return vars
}
// maskMix is the color of a coverage bitmask: the agent colors it stands for,
// averaged. The average is written as nested color-mix rather than mixed here,
// so the same rule holds for every theme the browser can switch to.
func (s *Site) maskMix(mask uint8) string {
var idx []int
for i, a := range s.Agents {
if a.Bit != 0 && mask&a.Bit != 0 {
idx = append(idx, i)
}
}
return mixExpr(idx)
}
func mixExpr(idx []int) string {
switch len(idx) {
case 0:
return "var(--fg)"
case 1:
return fmt.Sprintf("var(--ag%d)", idx[0])
}
return fmt.Sprintf("color-mix(in srgb, var(--ag%d) %.3f%%, %s)",
idx[0], 100/float64(len(idx)), mixExpr(idx[1:]))
}
// ── the switchable themes ─────────────────────────────────────────────────
// writeThemesCSS writes one block per theme found on this machine. It is only
// fetched once a theme has been picked (or the picker opened), so the page
// costs nothing for the reader who never switches.
func writeThemesCSS(w io.Writer, themes []*Theme, keys []string) {
fmt.Fprintf(w, "/* %d themes, found on this machine — set data-theme on :root to wear one */\n", len(themes))
for _, t := range themes {
fmt.Fprintf(w, ":root[data-theme=%q] {\n", t.ID)
for _, v := range themeVars(t, keys) {
fmt.Fprintf(w, " %s: %s;\n", v.K, v.V)
}
scheme := "dark"
if t.Appearance == "light" {
scheme = "light"
}
fmt.Fprintf(w, " color-scheme: %s;\n}\n", scheme)
}
}
// themeCard is a theme as the picker shows it: a name, where it came from, and
// the few colors that make it recognizable at a glance.
type themeCard struct {
ID string `json:"id"`
Name string `json:"name"`
Src string `json:"src"`
Dark bool `json:"dark"`
Sw []string `json:"sw"`
}
// writeThemesJS is the picker's menu. It is a script, not JSON, because the
// static export is opened from the filesystem, where fetch() is not allowed
// but a <script src> still loads.
func writeThemesJS(w io.Writer, themes []*Theme) {
cards := make([]themeCard, 0, len(themes))
for _, t := range themes {
syn := func(key string) string {
if k := resolveKey(t, key); k != "" {
return hex6(t.Syntax[k].Color)
}
return hex6(t.Fg)
}
cards = append(cards, themeCard{ID: t.ID, Name: t.Name, Src: t.Source,
Dark: t.Appearance != "light",
Sw: []string{hex6(t.Bg), hex6(t.Fg), hex6(t.Accent),
syn("keyword"), syn("string"), syn("function")}})
}
b, err := json.Marshal(cards)
if err != nil {
b = []byte("[]")
}
fmt.Fprintf(w, "// %d themes, written by notevi from this machine's zed and helix installs\n", len(cards))
fmt.Fprintf(w, "window.VRSITE_THEMES=%s;\n", strings.ReplaceAll(string(b), "},{", "},\n{"))
}
|