diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 23:43:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-03 09:54:39 -0300 |
| commit | 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch) | |
| tree | f12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/theme.go | |
| parent | a9263daee9413c5eff3c1f1bebcd224442fd8685 (diff) | |
| download | notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.tar.gz notevi-5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e.zip | |
rebrand to notevi: one CLI over the jj sidecar
vr and vrsite become a single binary. vrsite/ folds into a web package in
one module (0x4200.cafe/notevi); "notevi web" serves and exports exactly
what vrsite did, and "notevi read/grep/note/query" is unchanged.
The sidecar is renamed with it: notevi_log, notevi-log.jsonl, and the
description "private: notevi log". The pre-rebrand names are still
recognized, so an old repository opens and reads; it is renamed in place
on the first write, or up front with "notevi migrate DIR...".
That rename cannot be a single mv inside jj run. jj only auto-tracks a
*new* file in the run working copy below a size limit it does not take
from the command line, so writing a whole log under a name the change has
never held is silently dropped while jj reports success. ensureLogFile
creates the file empty first and lets every later byte be a modification
of a tracked file, which snapshots at any size; that also fixes the same
latent bug when importing a large legacy vr-log.jsonl.
Adds a bem-te-vi mark (favicon and nav brand) and a README.
Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
Diffstat (limited to 'vrsite/theme.go')
| -rw-r--r-- | vrsite/theme.go | 341 |
1 files changed, 0 insertions, 341 deletions
diff --git a/vrsite/theme.go b/vrsite/theme.go deleted file mode 100644 index 91eedbe..0000000 --- a/vrsite/theme.go +++ /dev/null @@ -1,341 +0,0 @@ -// theme.go — Zed theme JSON in, CSS custom properties out. Per-agent colors -// come from the theme's player (collaborator) palette. -// -// The two themes named on the command line are what the site is built with; -// themes.go finds the rest of this machine's themes for the picker, and they -// arrive here as the same *Theme. -package main - -import ( - "encoding/json" - "errors" - "fmt" - "os" - "sort" - "strings" -) - -type SyntaxStyle struct { - Color string `json:"color"` - FontStyle *string `json:"font_style"` - FontWeight *float64 `json:"font_weight"` -} - -type Theme struct { - ID, Source string // "zed/one-dark", "zed" | "helix" - Name, Appearance string - Bg, Fg, Panel, Border, LineNo, Muted, Accent string - Note, Hover string - Syntax map[string]SyntaxStyle - Players []string -} - -// defaultTheme finds a zed theme file containing a theme of the wanted -// appearance; "" means fall back to the embedded One family. -func defaultTheme(appearance string) string { - for _, path := range globAll(zedThemeGlobs()) { - b, err := os.ReadFile(path) - if err != nil { - continue - } - fam, err := parseZedFamily(b) - if err != nil { - continue - } - for _, t := range fam { - if t.Appearance == appearance { - return path - } - } - } - return "" -} - -func loadTheme(path, name, appearance string) *Theme { - var b []byte - var err error - if path == "" { - b, err = tfs.ReadFile("assets/one.json") - } else { - b, err = os.ReadFile(path) - } - if err != nil { - fatal("%v", err) - } - fam, err := parseZedFamily(b) - if err != nil { - fatal("bad zed theme %s: %v", path, err) - } - t := fam[0] - if name != "" { - for _, c := range fam { - if c.Name == name { - t = c - } - } - } else { - for _, c := range fam { - if c.Appearance == appearance { - t = c - break - } - } - } - return t -} - -// parseZedFamily reads a zed theme file: one file is a family, and every theme -// in it is one the picker can offer. -func parseZedFamily(b []byte) ([]*Theme, error) { - var family struct { - Name string `json:"name"` - Themes []struct { - Name string `json:"name"` - Appearance string `json:"appearance"` - Style map[string]json.RawMessage `json:"style"` - } `json:"themes"` - } - if err := json.Unmarshal(b, &family); err != nil { - return nil, err - } - if len(family.Themes) == 0 { - return nil, errors.New("no themes in it") - } - var out []*Theme - for _, th := range family.Themes { - get := func(keys ...string) string { - for _, k := range keys { - var s string - if raw, ok := th.Style[k]; ok && json.Unmarshal(raw, &s) == nil && okColor(s) != "" { - return s - } - } - return "" - } - t := &Theme{ID: "zed/" + slug(th.Name), Source: "zed", - Name: th.Name, Appearance: th.Appearance, - Bg: get("editor.background", "background"), - Fg: get("editor.foreground", "text"), - Panel: get("panel.background", "surface.background", "background"), - Border: get("border", "border.variant"), - LineNo: get("editor.line_number"), - Muted: get("text.muted", "text.disabled"), - Accent: get("text.accent", "info"), - Note: get("warning", "conflict", "modified"), - Hover: get("element.hover", "ghost_element.hover"), - } - json.Unmarshal(th.Style["syntax"], &t.Syntax) - for k, s := range t.Syntax { // a theme in the wild does drop the odd "#" - if okColor(s.Color) == "" { - delete(t.Syntax, k) - } - } - t.fill() - var players []struct { - Cursor string `json:"cursor"` - } - json.Unmarshal(th.Style["players"], &players) - var cursors []string - for _, p := range players { - cursors = append(cursors, p.Cursor) - } - t.setPlayers(cursors, t.syntaxColors()) - out = append(out, t) - } - return out, nil -} - -// setPlayers picks the per-agent colors out of the theme's own palette. A zed -// theme has one meant for exactly this — the collaborator cursors — and it is -// asked first; when it runs short, or the theme is a helix one with no such -// palette at all, the code's own colors stand in, since those are what the -// reader is already telling apart on the page. -// -// Within each tier, colors that vanish against fg/bg, collide with the note -// color, or sit on top of one already taken are dropped, and the rest ordered -// greedily for mutual distance, so the first few agents contrast most. -func (t *Theme) setPlayers(tiers ...[]string) { - t.Players = nil - taken := func(c string, of []string) bool { - for _, p := range of { - if colorDist(c, p) <= 60 { - return true - } - } - return false - } - for _, tier := range tiers { - var usable []string - for _, c := range tier { - c = hex6(c) - if len(c) != 7 || taken(c, usable) || taken(c, t.Players) || - colorDist(c, hex6(t.Fg)) <= 60 || colorDist(c, hex6(t.Bg)) <= 60 || - colorDist(c, hex6(t.Note)) <= 60 { - continue - } - usable = append(usable, c) - } - for len(usable) > 0 && len(t.Players) < maxAgents { - best, bestScore := 0, -1 - for i, c := range usable { - score := 1 << 24 // first pick of the first tier: keep theme order - for _, p := range t.Players { - if d := colorDist(c, p); d < score { - score = d - } - } - if score > bestScore { - best, bestScore = i, score - } - } - t.Players = append(t.Players, usable[best]) - usable = append(usable[:best], usable[best+1:]...) - } - } -} - -// fill supplies whatever a theme does not say. Editors leave gaps a web page -// cannot — a zed theme need not name a muted text color, a helix theme has no -// notion of a panel — so every gap is closed with a shade mixed from the -// theme's own background and foreground, or with a color out of its syntax. -// A theme with three colors in it still dresses a whole page. -func (t *Theme) fill() { - bg, fg := hex6(t.Bg), t.Fg - // panel and hover are backgrounds the page writes on, so a theme that - // offers one the text would be lost in does not get to keep it - t.Panel = first(readable(t.Panel, fg, ""), blend(bg, fg, 0.05)) - t.Hover = first(readable(t.Hover, fg, ""), blend(bg, fg, 0.12)) - t.Border = first(t.Border, blend(bg, fg, 0.2)) - t.LineNo = first(t.LineNo, blend(bg, fg, 0.4)) - t.Muted = first(readable(t.Muted, t.Panel, ""), blend(bg, fg, 0.55)) - t.Accent = first(t.Accent, t.syn("function"), t.syn("keyword"), t.syn("type"), fg) - t.Note = first(t.Note, t.syn("warning"), t.syn("number"), t.Accent) -} - -// syn is the theme's color for a syntax key, resolved along its dots. -func (t *Theme) syn(key string) string { - if k := resolveKey(t, key); k != "" { - return t.Syntax[k].Color - } - return "" -} - -// syntaxColors are the theme's own code colors, in the order they stand out -// from one another: what a theme offers when its palette runs short. -func (t *Theme) syntaxColors() []string { - var out []string - for _, k := range []string{"keyword", "function", "string", "type", "constant", - "variable.parameter", "attribute", "constructor", "number", "tag", "property", - "label", "namespace", "operator", "boolean"} { - if c := t.syn(k); c != "" { - out = append(out, c) - } - } - return out -} - -// readable keeps a color only if text in `on` can be seen against it. -func readable(c, on, fallback string) string { - if c == "" || colorDist(hex6(c), hex6(on)) < 2000 { - return fallback - } - return c -} - -// syntaxKeys is every syntax key the site paints, and so every one a theme is -// asked to fill: the union of what the two built-with themes name. -func syntaxKeys(themes ...*Theme) []string { - var keys []string - seen := map[string]bool{} - for _, t := range themes { - for k := range t.Syntax { - if !seen[k] { - seen[k] = true - keys = append(keys, k) - } - } - } - sort.Strings(keys) - return keys -} - -func colorDist(a, b string) int { - var ar, ag, ab, br, bg, bb int - if len(a) != 7 || len(b) != 7 { - return 1 << 24 - } - fmt.Sscanf(a[1:], "%02x%02x%02x", &ar, &ag, &ab) - fmt.Sscanf(b[1:], "%02x%02x%02x", &br, &bg, &bb) - d := func(x, y int) int { return (x - y) * (x - y) } - return int(float64(d(ar, br)+d(ag, bg)+d(ab, bb)) / 3) -} - -var fallbackPalette = []string{"#7aa2f7", "#e0af68", "#9ece6a", "#f7768e", "#bb9af7", "#2ac3de"} - -func (t *Theme) Player(i int) string { - if i < len(t.Players) { - return t.Players[i] - } - return fallbackPalette[i%len(fallbackPalette)] -} - -// okColor keeps a color only if it is one a browser will take: #rgb, #rrggbb -// or #rrggbbaa. Whatever else a theme file says is treated as unsaid, and -// fill() supplies it. -func okColor(c string) string { - if len(c) != 4 && len(c) != 7 && len(c) != 9 || c[0] != '#' { - return "" - } - for _, r := range c[1:] { - if !(r >= '0' && r <= '9' || r >= 'a' && r <= 'f' || r >= 'A' && r <= 'F') { - return "" - } - } - return c -} - -// hex6 strips an alpha channel from #rrggbbaa. -func hex6(c string) string { - if len(c) == 9 && c[0] == '#' { - return c[:7] - } - if len(c) == 4 && c[0] == '#' { // #rgb - return "#" + string([]byte{c[1], c[1], c[2], c[2], c[3], c[3]}) - } - return c -} - -func rgb(c string) (r, g, b int, ok bool) { - c = hex6(c) - if len(c) != 7 || c[0] != '#' { - return 0, 0, 0, false - } - if n, _ := fmt.Sscanf(c[1:], "%02x%02x%02x", &r, &g, &b); n != 3 { - return 0, 0, 0, false - } - return r, g, b, true -} - -// lum is perceived brightness, 0..1 — enough to tell a light theme from a -// dark one when the theme does not say which it is. -func lum(c string) float64 { - r, g, b, ok := rgb(c) - if !ok { - return 0 - } - return (0.299*float64(r) + 0.587*float64(g) + 0.114*float64(b)) / 255 -} - -// blend mixes frac of b into a — for the shades a theme leaves unsaid. -func blend(a, b string, frac float64) string { - ar, ag, ab, ok := rgb(a) - br, bg, bb, ok2 := rgb(b) - if !ok || !ok2 { - return a - } - mix := func(x, y int) int { return int(float64(x) + (float64(y)-float64(x))*frac) } - return fmt.Sprintf("#%02x%02x%02x", mix(ar, br), mix(ag, bg), mix(ab, bb)) -} - -// cssKey is a syntax key as it appears in class and property names. -func cssKey(k string) string { return strings.ReplaceAll(k, ".", "-") } |
