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/themes.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/themes.go')
| -rw-r--r-- | vrsite/themes.go | 445 |
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(), "-") -} |
