summaryrefslogtreecommitdiff
path: root/vrsite/themes.go
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-02 23:43:08 -0300
committerGabriel Schneider <[email protected]>2026-08-03 09:54:39 -0300
commit5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch)
treef12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/themes.go
parenta9263daee9413c5eff3c1f1bebcd224442fd8685 (diff)
downloadnotevi-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/themes.go')
-rw-r--r--vrsite/themes.go445
1 files changed, 0 insertions, 445 deletions
diff --git a/vrsite/themes.go b/vrsite/themes.go
deleted file mode 100644
index f185fab..0000000
--- a/vrsite/themes.go
+++ /dev/null
@@ -1,445 +0,0 @@
-// themes.go — the themes this machine already has. Zed keeps families of them
-// in JSON, Helix keeps one TOML each with a palette and single inheritance;
-// both are read into the same *Theme, so the browser can wear either.
-//
-// Discovery is best-effort: a theme that will not parse is skipped, never
-// fatal. What the site is built with stays what -theme and -light-theme say —
-// these only widen what the picker can offer.
-package main
-
-import (
- "bytes"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strings"
-
- "github.com/BurntSushi/toml"
-)
-
-// themeSet is those themes plus the two files the browser switches with: one
-// CSS block per theme, and the list the picker draws from.
-type themeSet struct {
- List []*Theme
- CSS []byte // themes.css — one :root[data-theme="…"] block per theme
- JS []byte // themes.js — window.VRSITE_THEMES, the picker's menu
-}
-
-func newThemeSet(dark, light *Theme) *themeSet {
- return themesFrom(discoverThemes(), dark, light)
-}
-
-// themesFrom writes the two files for a given list of themes. The keys the
-// site paints with come from the themes it was built with, since those are the
-// ones the stylesheet has classes for.
-func themesFrom(list []*Theme, dark, light *Theme) *themeSet {
- ts := &themeSet{List: list}
- keys := syntaxKeys(dark, light)
- var css, js bytes.Buffer
- writeThemesCSS(&css, ts.List, keys)
- writeThemesJS(&js, ts.List)
- ts.CSS, ts.JS = css.Bytes(), js.Bytes()
- return ts
-}
-
-func (ts *themeSet) count(source string) int {
- n := 0
- for _, t := range ts.List {
- if t.Source == source {
- n++
- }
- }
- return n
-}
-
-// discoverThemes reads every theme both editors can see, user directories
-// first so a theme you installed wins the id over one that merely ships.
-func discoverThemes() []*Theme {
- var out []*Theme
- seen := map[string]bool{}
- add := func(t *Theme) {
- if t == nil || t.ID == "" || t.Bg == "" || t.Fg == "" || seen[t.ID] {
- return
- }
- seen[t.ID] = true
- out = append(out, t)
- }
- 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 {
- add(t)
- }
- }
- dirs := helixThemeDirs()
- for _, dir := range dirs {
- for _, path := range globAll([]string{filepath.Join(dir, "*.toml")}) {
- add(loadHelixTheme(path, dirs))
- }
- }
- sort.Slice(out, func(i, j int) bool {
- if out[i].Source != out[j].Source {
- return out[i].Source < out[j].Source
- }
- return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
- })
- return out
-}
-
-func globAll(pats []string) []string {
- var out []string
- for _, p := range pats {
- m, _ := filepath.Glob(p)
- sort.Strings(m)
- out = append(out, m...)
- }
- return out
-}
-
-func configHome() string {
- if d := os.Getenv("XDG_CONFIG_HOME"); d != "" {
- return d
- }
- home, _ := os.UserHomeDir()
- return filepath.Join(home, ".config")
-}
-
-func dataHome() string {
- if d := os.Getenv("XDG_DATA_HOME"); d != "" {
- return d
- }
- home, _ := os.UserHomeDir()
- return filepath.Join(home, ".local", "share")
-}
-
-// zedThemeGlobs: themes you dropped in yourself, then the ones extensions
-// brought. Zed's own built-ins live inside the binary, so they are not here.
-func zedThemeGlobs() []string {
- home, _ := os.UserHomeDir()
- mac := filepath.Join(home, "Library", "Application Support", "Zed")
- return []string{
- filepath.Join(configHome(), "zed", "themes", "*.json"),
- filepath.Join(mac, "themes", "*.json"),
- filepath.Join(dataHome(), "zed", "extensions", "installed", "*", "themes", "*.json"),
- filepath.Join(mac, "extensions", "installed", "*", "themes", "*.json"),
- }
-}
-
-// helixThemeDirs: yours first, then whichever runtime the installed hx uses —
-// found the way helix itself looks, by walking out from the binary.
-func helixThemeDirs() []string {
- home, _ := os.UserHomeDir()
- dirs := []string{filepath.Join(configHome(), "helix", "themes")}
- var runtimes []string
- if r := os.Getenv("HELIX_RUNTIME"); r != "" {
- runtimes = append(runtimes, r)
- }
- runtimes = append(runtimes,
- filepath.Join(configHome(), "helix", "runtime"),
- filepath.Join(dataHome(), "helix", "runtime"))
- if bin, err := exec.LookPath("hx"); err == nil {
- if bin, err = filepath.EvalSymlinks(bin); err == nil {
- dir := filepath.Dir(bin)
- runtimes = append(runtimes,
- filepath.Join(dir, "runtime"),
- filepath.Join(filepath.Dir(dir), "lib", "helix", "runtime"),
- filepath.Join(filepath.Dir(dir), "share", "helix", "runtime"))
- }
- }
- runtimes = append(runtimes,
- "/usr/lib/helix/runtime", "/usr/share/helix/runtime",
- "/usr/local/lib/helix/runtime", "/opt/homebrew/opt/helix/libexec/runtime",
- filepath.Join(home, ".cargo", "share", "helix", "runtime"))
- seen := map[string]bool{}
- for _, r := range runtimes {
- d := filepath.Join(r, "themes")
- if seen[d] {
- continue
- }
- seen[d] = true
- if fi, err := os.Stat(d); err == nil && fi.IsDir() {
- dirs = append(dirs, d)
- }
- }
- return dirs
-}
-
-// ── helix ─────────────────────────────────────────────────────────────────
-//
-// A helix theme is scope -> style, with colors named out of a [palette] at the
-// bottom, and may inherit one other theme by name. Inheritance merges both
-// halves before anything resolves, which is what lets a child theme restate
-// the palette alone (catppuccin_latte is catppuccin_mocha with new colors).
-
-type helixStyle struct {
- fg, bg string // palette names, hex, or one of the sixteen terminal names
- italic bool
- bold bool
-}
-
-type helixRaw struct {
- scopes map[string]helixStyle
- palette map[string]string
-}
-
-func loadHelixTheme(path string, dirs []string) *Theme {
- raw := readHelixRaw(path, dirs, 0)
- if raw == nil {
- return nil
- }
- name := strings.TrimSuffix(filepath.Base(path), ".toml")
- return helixTheme(name, raw)
-}
-
-func readHelixRaw(path string, dirs []string, depth int) *helixRaw {
- b, err := os.ReadFile(path)
- if err != nil {
- return nil
- }
- var doc map[string]any
- if err := toml.Unmarshal(b, &doc); err != nil {
- return nil
- }
- raw := &helixRaw{scopes: map[string]helixStyle{}, palette: map[string]string{}}
- if parent, _ := doc["inherits"].(string); parent != "" && depth < 8 {
- if p := findHelixTheme(parent, dirs); p != "" {
- if base := readHelixRaw(p, dirs, depth+1); base != nil {
- raw = base
- }
- }
- }
- if pal, ok := doc["palette"].(map[string]any); ok {
- for k, v := range pal {
- if s, ok := v.(string); ok {
- raw.palette[k] = s
- }
- }
- }
- delete(doc, "palette")
- delete(doc, "inherits")
- flattenHelix("", doc, raw.scopes)
- return raw
-}
-
-func findHelixTheme(name string, dirs []string) string {
- if name == "" || strings.ContainsAny(name, "/\\.") {
- return ""
- }
- for _, d := range dirs {
- p := filepath.Join(d, name+".toml")
- if _, err := os.Stat(p); err == nil {
- return p
- }
- }
- return ""
-}
-
-// flattenHelix walks the decoded document into scope -> style. A table is a
-// style when it holds only style keys; anything else is a dotted scope name
-// someone wrote out as a table, so it keeps descending.
-func flattenHelix(prefix string, m map[string]any, into map[string]helixStyle) {
- for k, v := range m {
- key := k
- if prefix != "" {
- key = prefix + "." + k
- }
- switch v := v.(type) {
- case string:
- into[key] = helixStyle{fg: v}
- case map[string]any:
- if st, ok := helixStyleOf(v); ok {
- into[key] = st
- } else {
- flattenHelix(key, v, into)
- }
- }
- }
-}
-
-func helixStyleOf(m map[string]any) (helixStyle, bool) {
- var st helixStyle
- for k, v := range m {
- switch k {
- case "fg":
- st.fg, _ = v.(string)
- case "bg":
- st.bg, _ = v.(string)
- case "modifiers":
- mods, _ := v.([]any)
- for _, mod := range mods {
- switch mod {
- case "italic":
- st.italic = true
- case "bold":
- st.bold = true
- }
- }
- case "underline", "underline_style", "underline_color":
- // decoration we have no use for, but still a style
- default:
- return helixStyle{}, false
- }
- }
- return st, true
-}
-
-// the sixteen names a helix theme may use without a palette entry
-var termHex = map[string]string{
- "black": "#000000", "red": "#cd3131", "green": "#0dbc79", "yellow": "#e5e510",
- "blue": "#2472c8", "magenta": "#bc3fbc", "cyan": "#11a8cd", "gray": "#8a8a8a",
- "light-red": "#f14c4c", "light-green": "#23d18b", "light-yellow": "#f5f543",
- "light-blue": "#3b8eea", "light-magenta": "#d670d6", "light-cyan": "#29b8db",
- "light-gray": "#c0c0c0", "white": "#e5e5e5",
-}
-
-func (r *helixRaw) color(name string) string {
- if name == "" || name == "default" {
- return ""
- }
- if c, ok := r.palette[name]; ok {
- name = c
- }
- if strings.HasPrefix(name, "#") {
- return hex6(okColor(name))
- }
- return termHex[name]
-}
-
-// fg / bg take the first of several scopes that names one, so a theme that
-// skips a scope is answered by whatever it does say. Scope names fall back
-// along their dots the way helix resolves them.
-func (r *helixRaw) fg(scopes ...string) string { return r.pick(true, scopes) }
-func (r *helixRaw) bg(scopes ...string) string { return r.pick(false, scopes) }
-
-func (r *helixRaw) pick(wantFg bool, scopes []string) string {
- for _, scope := range scopes {
- for key := scope; key != ""; {
- if st, ok := r.scopes[key]; ok {
- c := st.bg
- if wantFg {
- c = st.fg
- }
- if c := r.color(c); c != "" {
- return c
- }
- }
- i := strings.LastIndex(key, ".")
- if i < 0 {
- break
- }
- key = key[:i]
- }
- }
- return ""
-}
-
-// zed syntax keys helix spells differently; everything else either matches or
-// falls back along its dots (punctuation.bracket -> punctuation).
-var helixAlias = map[string]string{
- "number": "constant.numeric",
- "boolean": "constant.builtin.boolean",
- "property": "variable.other.member",
- "preproc": "keyword.directive",
- "primary": "variable",
- "enum": "type.enum",
- "string.escape": "constant.character.escape",
- "string.regex": "string.regexp",
- "variable.special": "variable.builtin",
- "comment.doc": "comment.block.documentation",
- "link_uri": "markup.link.url",
- "link_text": "markup.link.text",
- "title": "markup.heading",
- "emphasis": "markup.italic",
- "emphasis.strong": "markup.bold",
-}
-
-func helixTheme(name string, raw *helixRaw) *Theme {
- bg := raw.bg("ui.background", "ui.window")
- fg := raw.fg("ui.text", "ui.window")
- if bg == "" || fg == "" {
- return nil // a theme that paints on the terminal's colors, not ours
- }
- t := &Theme{
- ID: "helix/" + slug(name), Source: "helix", Name: name,
- Appearance: "dark", Bg: bg, Fg: fg,
- Syntax: map[string]SyntaxStyle{},
- }
- if lum(bg) > 0.5 {
- t.Appearance = "light"
- }
- // A helix theme dresses an editor, not a web page, so it is read for what
- // the page needs and fill() closes the rest. The quiet backgrounds come
- // first: a selected menu row is often the theme's loudest color.
- t.Panel = raw.bg("ui.statusline", "ui.menu", "ui.popup")
- t.Hover = raw.bg("ui.cursorline.primary", "ui.selection", "ui.menu.selected")
- t.Border = raw.fg("ui.window")
- t.LineNo = raw.fg("ui.linenr")
- t.Muted = raw.fg("comment", "ui.linenr")
- t.Accent = raw.fg("function", "keyword", "type")
- t.Note = raw.fg("warning", "diagnostic.warning", "constant.numeric")
-
- for scope, st := range raw.scopes {
- if strings.HasPrefix(scope, "ui.") {
- continue
- }
- if c := raw.color(st.fg); c != "" {
- t.Syntax[scope] = syntaxStyle(c, st.italic, st.bold)
- }
- }
- for key, scope := range helixAlias {
- if _, ok := t.Syntax[key]; ok {
- continue
- }
- if st, ok := t.Syntax[resolveKey(t, scope)]; ok {
- t.Syntax[key] = st
- }
- }
- t.fill()
- t.setPlayers(t.syntaxColors())
- return t
-}
-
-func syntaxStyle(color string, italic, bold bool) SyntaxStyle {
- s := SyntaxStyle{Color: color}
- if italic {
- it := "italic"
- s.FontStyle = &it
- }
- if bold {
- w := 700.0
- s.FontWeight = &w
- }
- return s
-}
-
-func first(vals ...string) string {
- for _, v := range vals {
- if v != "" {
- return v
- }
- }
- return ""
-}
-
-// slug is a theme's id in an attribute and a URL: ascii, lowercase, dashed.
-func slug(s string) string {
- var b strings.Builder
- dash := false
- for _, r := range strings.ToLower(s) {
- if r >= 'a' && r <= 'z' || r >= '0' && r <= '9' {
- b.WriteRune(r)
- dash = false
- } else if !dash && b.Len() > 0 {
- b.WriteByte('-')
- dash = true
- }
- }
- return strings.Trim(b.String(), "-")
-}