summaryrefslogtreecommitdiff
path: root/vrsite/themes_test.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_test.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_test.go')
-rw-r--r--vrsite/themes_test.go294
1 files changed, 0 insertions, 294 deletions
diff --git a/vrsite/themes_test.go b/vrsite/themes_test.go
deleted file mode 100644
index 4ef1cca..0000000
--- a/vrsite/themes_test.go
+++ /dev/null
@@ -1,294 +0,0 @@
-package main
-
-import (
- "os"
- "path/filepath"
- "strings"
- "testing"
-)
-
-// everything a helix theme reader has to survive: quoted scope keys, a
-// palette, one of the terminal color names, modifiers, and an inline table
-// broken over lines the way a formatter leaves it
-const helixSrc = `
-# a comment
-"ui.background" = { bg = "base" }
-"ui.text" = { fg = "text" }
-"ui.linenr" = { fg = "muted" }
-"ui.statusline" = { fg = "text", bg = "surface" }
-"ui.cursorline.primary" = { bg = "highlight" }
-"ui.menu.selected" = { fg = "base", bg = "rose" }
-"ui.window" = { fg = "muted" }
-comment = { fg = "muted", modifiers = ["italic"] }
-keyword = { fg = "pine", modifiers = [
- "bold",
-] }
-string = "gold"
-function = { fg = "rose" }
-"constant.numeric" = { fg = "iris" }
-"variable.other.member" = { fg = "foam" }
-"punctuation" = { fg = "muted" }
-warning = { fg = "gold" }
-type = "white"
-
-[palette]
-base = "#191724"
-text = "#e0def4"
-muted = "#6e6a86"
-surface = "#1f1d2e"
-highlight = "#21202e"
-pine = "#31748f"
-gold = "#f6c177"
-rose = "#ebbcba"
-iris = "#c4a7e7"
-foam = "#9ccfd8"
-`
-
-func writeTheme(t *testing.T, dir, name, body string) string {
- t.Helper()
- if err := os.MkdirAll(dir, 0o755); err != nil {
- t.Fatal(err)
- }
- path := filepath.Join(dir, name)
- if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
- t.Fatal(err)
- }
- return path
-}
-
-func TestHelixTheme(t *testing.T) {
- dir := t.TempDir()
- path := writeTheme(t, dir, "rose_pine.toml", helixSrc)
- th := loadHelixTheme(path, []string{dir})
- if th == nil {
- t.Fatal("theme did not load")
- }
- for _, tc := range []struct{ what, got, want string }{
- {"id", th.ID, "helix/rose-pine"},
- {"source", th.Source, "helix"},
- {"name", th.Name, "rose_pine"},
- {"appearance", th.Appearance, "dark"},
- {"bg", th.Bg, "#191724"},
- {"fg", th.Fg, "#e0def4"},
- {"panel", th.Panel, "#1f1d2e"},
- {"line numbers", th.LineNo, "#6e6a86"},
- {"border", th.Border, "#6e6a86"},
- {"accent", th.Accent, "#ebbcba"}, // function
- {"note", th.Note, "#f6c177"}, // warning
- {"hover", th.Hover, "#21202e"}, // cursorline, not the loud menu row
- {"muted", th.Muted, "#6e6a86"}, // comment
- } {
- if tc.got != tc.want {
- t.Errorf("%s = %q, want %q", tc.what, tc.got, tc.want)
- }
- }
- // syntax keys the site paints with, spelled the way helix spells them
- for _, tc := range []struct{ key, want string }{
- {"comment", "#6e6a86"},
- {"keyword", "#31748f"},
- {"string", "#f6c177"},
- {"type", "#e5e5e5"}, // a terminal color name, not a palette one
- {"number", "#c4a7e7"}, // zed's name for constant.numeric
- {"property", "#9ccfd8"}, // zed's name for variable.other.member
- {"punctuation.bracket", "#6e6a86"}, // falls back along its dots
- } {
- if got := th.syn(tc.key); got != tc.want {
- t.Errorf("syntax %s = %q, want %q", tc.key, got, tc.want)
- }
- }
- if s := th.Syntax["comment"]; s.FontStyle == nil || *s.FontStyle != "italic" {
- t.Errorf("comment is not italic: %+v", s)
- }
- if s := th.Syntax["keyword"]; s.FontWeight == nil || *s.FontWeight != 700 {
- t.Errorf("keyword is not bold: %+v", s)
- }
- // agent colors: from the code's own colors, and none of them invisible
- if len(th.Players) < 3 {
- t.Fatalf("agent colors = %v, want at least 3", th.Players)
- }
- for _, p := range th.Players {
- if colorDist(p, hex6(th.Bg)) <= 60 || colorDist(p, hex6(th.Fg)) <= 60 {
- t.Errorf("agent color %s vanishes against the theme", p)
- }
- }
-}
-
-// A child theme restating only the palette is how helix ships most of its
-// families: the parent's scopes have to resolve against the child's colors.
-func TestHelixInherits(t *testing.T) {
- dir := t.TempDir()
- writeTheme(t, dir, "parent.toml", helixSrc)
- path := writeTheme(t, dir, "child.toml", `
-inherits = "parent"
-comment = { fg = "muted" }
-
-[palette]
-base = "#eff1f5"
-text = "#4c4f69"
-muted = "#7c7f93"
-surface = "#e6e9ef"
-highlight = "#dce0e8"
-pine = "#1e66f5"
-gold = "#df8e1d"
-rose = "#ea76cb"
-iris = "#8839ef"
-foam = "#04a5e5"
-`)
- th := loadHelixTheme(path, []string{dir})
- if th == nil {
- t.Fatal("child theme did not load")
- }
- if th.Bg != "#eff1f5" || th.Fg != "#4c4f69" {
- t.Errorf("inherited scopes kept the parent's palette: bg=%s fg=%s", th.Bg, th.Fg)
- }
- if th.Appearance != "light" {
- t.Errorf("appearance = %q, want light for a pale background", th.Appearance)
- }
- if got := th.syn("keyword"); got != "#1e66f5" {
- t.Errorf("inherited keyword = %q, want the child's pine", got)
- }
- // the child's own entry wins over the parent's, modifiers and all
- if s := th.Syntax["comment"]; s.FontStyle != nil {
- t.Errorf("child comment kept the parent's italics: %+v", s)
- }
-}
-
-// A theme that names almost nothing still has to dress a whole page.
-func TestFillClosesTheGaps(t *testing.T) {
- th := &Theme{Bg: "#101010", Fg: "#f0f0f0", Syntax: map[string]SyntaxStyle{
- "function": {Color: "#88c0d0"},
- }}
- th.fill()
- for _, tc := range []struct{ what, got string }{
- {"panel", th.Panel}, {"border", th.Border}, {"line numbers", th.LineNo},
- {"muted", th.Muted}, {"accent", th.Accent}, {"note", th.Note}, {"hover", th.Hover},
- } {
- if okColor(tc.got) == "" {
- t.Errorf("%s = %q, want a color", tc.what, tc.got)
- }
- }
- if th.Accent != "#88c0d0" {
- t.Errorf("accent = %q, want the theme's own function color", th.Accent)
- }
- if th.Panel == th.Bg || th.Hover == th.Bg {
- t.Errorf("panel %q and hover %q must lift off the background %q", th.Panel, th.Hover, th.Bg)
- }
- // a panel the text would be lost in is not kept
- lost := &Theme{Bg: "#f7f3ee", Fg: "#605a52", Panel: "#605a52"}
- lost.fill()
- if lost.Panel == "#605a52" {
- t.Error("kept a panel the foreground disappears into")
- }
-}
-
-func TestZedFamily(t *testing.T) {
- fam, err := parseZedFamily([]byte(`{"name":"Fake","themes":[
- {"name":"Fake Dark","appearance":"dark","style":{
- "editor.background":"#101010","editor.foreground":"#e0e0e0",
- "text.accent":"0f4b6e","warning":"#e0af68",
- "players":[{"cursor":"#7aa2f7"},{"cursor":"#9ece6a"}],
- "syntax":{"keyword":{"color":"#bb9af7","font_style":"italic"},
- "string":{"color":"9ece6a"}}}},
- {"name":"Fake Light","appearance":"light","style":{
- "editor.background":"#fafafa","editor.foreground":"#383a42"}}]}`))
- if err != nil {
- t.Fatal(err)
- }
- if len(fam) != 2 {
- t.Fatalf("themes = %d, want 2", len(fam))
- }
- dark, light := fam[0], fam[1]
- if dark.ID != "zed/fake-dark" || light.ID != "zed/fake-light" {
- t.Errorf("ids = %q, %q", dark.ID, light.ID)
- }
- // a theme in the wild does drop the odd "#": what is not a color is
- // treated as unsaid, and filled in rather than emitted as nonsense
- if dark.Accent == "0f4b6e" {
- t.Error("kept a color with no #")
- }
- if _, ok := dark.Syntax["string"]; ok {
- t.Error("kept a syntax color with no #")
- }
- if dark.syn("keyword") != "#bb9af7" {
- t.Errorf("keyword = %q", dark.syn("keyword"))
- }
- if len(dark.Players) < 2 || dark.Players[0] != "#7aa2f7" {
- t.Errorf("agent colors = %v, want the player palette first", dark.Players)
- }
- // nothing to work from but bg and fg, and still a whole theme
- if okColor(light.Muted) == "" || okColor(light.Hover) == "" {
- t.Errorf("light theme left gaps: %+v", light)
- }
-}
-
-// The picker's two files: a block per theme, and a menu that names them.
-func TestThemeFiles(t *testing.T) {
- dark, light := loadTheme("", "", "dark"), loadTheme("", "", "light")
- helix := loadHelixTheme(writeTheme(t, t.TempDir(), "rose_pine.toml", helixSrc), nil)
- ts := themesFrom([]*Theme{dark, light, helix}, dark, light)
-
- css := string(ts.CSS)
- for _, th := range ts.List {
- if !strings.Contains(css, `:root[data-theme="`+th.ID+`"]`) {
- t.Errorf("no css block for %s", th.ID)
- }
- if !strings.Contains(string(ts.JS), th.ID) {
- t.Errorf("%s is not in the menu", th.ID)
- }
- }
- for _, want := range []string{"--bg:", "--fg:", "--ag0:", "color-scheme: dark", "color-scheme: light"} {
- if !strings.Contains(css, want) {
- t.Errorf("themes.css never says %q", want)
- }
- }
- // every declaration has to be worth something to the browser
- for _, line := range strings.Split(css, "\n") {
- if l := strings.TrimSpace(line); strings.HasPrefix(l, "--") && strings.HasSuffix(l, ": ;") {
- t.Errorf("empty declaration: %s", l)
- }
- }
- if n := strings.Count(string(ts.JS), `"id":`); n != len(ts.List) {
- t.Errorf("menu has %d themes, want %d", n, len(ts.List))
- }
-}
-
-// The two editors' theme directories, as they are on a machine that has them.
-func TestDiscoverFindsBothEditors(t *testing.T) {
- home := t.TempDir()
- t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "config"))
- t.Setenv("XDG_DATA_HOME", filepath.Join(home, "data"))
- writeTheme(t, filepath.Join(home, "config", "zed", "themes"), "mine.json",
- `{"name":"Mine","themes":[{"name":"Mine Dark","appearance":"dark","style":{
- "editor.background":"#101010","editor.foreground":"#e0e0e0"}}]}`)
- writeTheme(t, filepath.Join(home, "data", "zed", "extensions", "installed", "ext", "themes"),
- "ext.json", `{"name":"Ext","themes":[{"name":"Ext Light","appearance":"light","style":{
- "editor.background":"#fafafa","editor.foreground":"#383a42"}}]}`)
- writeTheme(t, filepath.Join(home, "config", "helix", "themes"), "mine_helix.toml", helixSrc)
-
- found := map[string]*Theme{}
- for _, th := range discoverThemes() {
- found[th.ID] = th
- }
- for _, want := range []string{"zed/mine-dark", "zed/ext-light", "helix/mine-helix"} {
- if found[want] == nil {
- t.Errorf("did not find %s", want)
- }
- }
-}
-
-// Coverage colors are mixed in the browser so every theme can supply them.
-func TestMaskMix(t *testing.T) {
- s := &Site{Agents: []*Agent{{Bit: 1}, {Bit: 2}, {Bit: 4}}}
- for _, tc := range []struct {
- mask uint8
- want string
- }{
- {1, "var(--ag0)"},
- {5, "color-mix(in srgb, var(--ag0) 50.000%, var(--ag2))"},
- {7, "color-mix(in srgb, var(--ag0) 33.333%, color-mix(in srgb, var(--ag1) 50.000%, var(--ag2)))"},
- } {
- if got := s.maskMix(tc.mask); got != tc.want {
- t.Errorf("mask %d = %q, want %q", tc.mask, got, tc.want)
- }
- }
-}