the language meter stands on the repo home
commit 86d336b1486bd91ef7b30d4d38a70a5ce1f80d7d · parent 370d8f1c9f
· browse this tree
Ximianth, 2026-10-26 23:03:42 UTC
A segmented bar in a rough box between the plaque and the clone line: top languages by bytes at the shown commit, the tail (and any share that rounds below a visible 0.1%) folded into a neutral "other". Linguist's conventional hues color the segments and the legend's swatches -- swatches only, never text contrast; names and percentages stay ink, museum-label sized. A faint inset ring keeps pale swatches from dissolving into the paper in either scheme.
diff --git a/chrome.go b/chrome.go index c291707..51a9152 100644 --- a/chrome.go +++ b/chrome.go @@ -200,6 +200,47 @@ article .tagline { article p.clone, article #branches { text-align: center; margin-left: auto; margin-right: auto; } article p.clone.boxed { display: block; width: fit-content; } +/* The language meter: composition by bytes at the shown commit, in its rough box + between the plaque and the clone line. Colors are linguist's conventions, used as + swatches only; names and percentages stay ink. The faint inset ring keeps pale + swatches from dissolving into the paper (or the dark ground). */ +article .langmeter { + display: block; + width: fit-content; + max-width: 100%; + margin: 0 auto 1.4rem; +} +.langmeter .langbar { + display: flex; + gap: 3px; + width: 28rem; + max-width: 100%; + height: 12px; +} +.langmeter .langbar .seg { + flex-basis: 0; + min-width: 2px; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 25%, transparent); +} +.langmeter .langchips { + margin: .6rem 0 0; + font-size: .78rem; + letter-spacing: .08em; + color: var(--ink-soft); + text-align: center; +} +.langmeter .chip { white-space: nowrap; } +.langmeter .chip + .chip { margin-left: .9rem; } +.langmeter .swatch { + display: inline-block; + width: 10px; + height: 10px; + margin-right: .35em; + border-radius: 2px; + box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 25%, transparent); +} +.langmeter .seg.other, .langmeter .swatch.other { background: var(--ink-soft); } + /* The plaque: museum-label typography for the metadata line under an exhibit. */ .plaque { text-align: center; diff --git a/pages.go b/pages.go index 0396928..55f32c2 100644 --- a/pages.go +++ b/pages.go @@ -11,6 +11,8 @@ import ( "sort" "strings" + "github.com/go-enry/go-enry/v2" + "vitrine/display" ) @@ -124,14 +126,15 @@ var homeTmpl = template.Must(template.New("home").Parse(`{{if .Description}}<p c <p class="clone" id="clone">clone: <code>git clone {{.CloneURL}}</code></p> {{else}}{{if .Readme}}<div class="exhibit">{{.Readme}}</div> <p class="plaque">{{.ReadmeName}} · {{.ReadmeSize}} · {{.Ref}} = <code>{{.ShortSha}}</code></p> +{{end}}{{if .Langs}}<div class="langmeter boxed"> +<div class="langbar">{{range .Langs}}<span class="seg{{if not .Color}} other{{end}}" style="flex-grow:{{.Pct}}{{if .Color}};background:{{.Color}}{{end}}" title="{{.Name}} {{.Pct}}%"></span>{{end}}</div> +<p class="langchips">{{range .Langs}}<span class="chip"><span class="swatch{{if not .Color}} other{{end}}"{{if .Color}} style="background:{{.Color}}"{{end}}></span>{{.Name}} {{.Pct}}%</span>{{end}}</p> +</div> {{end}}<p class="clone boxed" id="clone">clone: <code>git clone {{.CloneURL}}</code></p> <p id="branches">branches: {{range $i, $b := .Branches}}{{if $i}}, {{end}}<a href="{{$b.Href}}">{{$b.Name}}</a>{{end}}{{if .Tags}} · tags: {{range $i, $t := .Tags}}{{if $i}}, {{end}}<a href="{{$t.Href}}">{{$t.Name}}</a>{{end}}{{end}}</p> {{end}}`)) -// The language meter will stand on the home page in a later round: -// a segmented bar in a rough frame with a chip legend, between the plaque and the clone box. - type refLink struct { Name string Href string @@ -148,6 +151,46 @@ type homeData struct { ReadmeSize string // human-readable; the plaque is a label, not a listing Ref string ShortSha string + Langs []langSeg +} + +// meterMax caps the bar's segments; everything past it folds into "other". +const meterMax = 8 + +// langSeg is one segment of the language meter, as the template wants it. +type langSeg struct { + Name string + Pct string // share of counted bytes, one decimal; doubles as the segment's flex-grow + Color string // linguist's conventional hex, a swatch only; empty wears the neutral class +} + +// langSegments shapes language stats for the meter: the top languages by bytes, +// with the tail -- and anything whose share rounds below a visible 0.1% -- folded +// into one trailing "other" segment. No classified bytes at all means no meter. +func langSegments(stats []langStat) []langSeg { + var total int64 + for _, st := range stats { + total += st.bytes + } + if total == 0 { + return nil + } + pct := func(bytes int64) string { + return fmt.Sprintf("%.1f", float64(bytes)*100/float64(total)) + } + var segs []langSeg + var other int64 + for i, st := range stats { + if i >= meterMax-1 && len(stats) > meterMax || pct(st.bytes) == "0.0" { + other += st.bytes + continue + } + segs = append(segs, langSeg{Name: st.name, Pct: pct(st.bytes), Color: enry.GetColor(st.name)}) + } + if other > 0 { + segs = append(segs, langSeg{Name: "other", Pct: pct(other)}) + } + return segs } func (s *server) serveHome(w http.ResponseWriter, r *http.Request, rp *repo) { @@ -182,6 +225,12 @@ func (s *server) serveHome(w http.ResponseWriter, r *http.Request, rp *repo) { return } d.ReadmeName, d.ReadmeSize, d.ShortSha = rm.name, humanSize(rm.size), shortSha(sha) + stats, err := s.langStats(rp, sha) + if err != nil { + httpError(w, err) + return + } + d.Langs = langSegments(stats) case errors.Is(err, errNotFound): d.Empty = true // an unborn HEAD: a freshly-init'd repo is fine to show, just bare default: diff --git a/pages_test.go b/pages_test.go index 8d8ee42..0993c5b 100644 --- a/pages_test.go +++ b/pages_test.go @@ -88,3 +88,41 @@ func TestHumanSize(t *testing.T) { } } } + +// TestLangSegments pins the meter's shaping: percentages, color lookup, the fold of the +// tail and of invisible shares into "other", and the no-classified-bytes case. +func TestLangSegments(t *testing.T) { + t.Parallel() + t.Run("empty stats make no meter", func(t *testing.T) { + if segs := langSegments(nil); segs != nil { + t.Errorf("segments = %+v, want none", segs) + } + }) + t.Run("plain shares with linguist colors", func(t *testing.T) { + segs := langSegments([]langStat{{"Go", 750}, {"Rust", 250}}) + want := []langSeg{{"Go", "75.0", "#00ADD8"}, {"Rust", "25.0", "#dea584"}} + if len(segs) != len(want) || segs[0] != want[0] || segs[1] != want[1] { + t.Errorf("segments = %+v, want %+v", segs, want) + } + }) + t.Run("the tail folds into other", func(t *testing.T) { + stats := make([]langStat, 12) + for i := range stats { + stats[i] = langStat{string(rune('A' + i)), int64(1200 - 100*i)} + } + segs := langSegments(stats) + if len(segs) != meterMax { + t.Fatalf("got %d segments (%+v), want %d", len(segs), segs, meterMax) + } + last := segs[meterMax-1] + if last.Name != "other" || last.Color != "" { + t.Errorf("last segment = %+v, want the neutral other", last) + } + }) + t.Run("invisible shares fold into other", func(t *testing.T) { + segs := langSegments([]langStat{{"Go", 100000}, {"Rust", 10}}) + if len(segs) != 2 || segs[1].Name != "other" { + t.Fatalf("segments = %+v, want Go and other", segs) + } + }) +} diff --git a/testdata/pages.md b/testdata/pages.md index fc9f989..7356d41 100644 --- a/testdata/pages.md +++ b/testdata/pages.md @@ -94,6 +94,10 @@ Content-Type: text/html; charset=utf-8 <p>A repo that exists to be looked at.</p> </div> <p class="plaque">README.md · 49 B · main = <code>ced91a8519</code></p> +<div class="langmeter boxed"> +<div class="langbar"><span class="seg" style="flex-grow:69.9;background:#00ADD8" title="Go 69.9%"></span><span class="seg" style="flex-grow:20.3;background:#dea584" title="Rust 20.3%"></span><span class="seg" style="flex-grow:9.9;background:#663399" title="CSS 9.9%"></span></div> +<p class="langchips"><span class="chip"><span class="swatch" style="background:#00ADD8"></span>Go 69.9%</span><span class="chip"><span class="swatch" style="background:#dea584"></span>Rust 20.3%</span><span class="chip"><span class="swatch" style="background:#663399"></span>CSS 9.9%</span></p> +</div> <p class="clone boxed" id="clone">clone: <code>git clone http://socle.example/fixturama</code></p> <p id="branches">branches: <a href="/fixturama/tree/dev/">dev</a>, <a href="/fixturama/tree/main/">main</a> · tags: <a href="/fixturama/tree/v1.0/">v1.0</a></p>