cat / 012
clone

a spreadsheet for the terminal: lotus 1-2-3 looks, google sheets keys and formulas, one go binary

Add VHS demo tapes and make demos

Dami committed 50dbc93
Ten short tapes in demos/ record first steps, selection stats, menus and
the palette, find and replace, formats, charts, freeze/sort/filter, CSV
import, JEV functions and decimal arithmetic, in the dark reference
palette. make demos renders them into demos/out/ with smaller copies for
the README in demos/out/media/. The JEV tape talks to demos/fakejev, a
stand-in service that answers by keywords, never the real one. A ttyd
wrapper makes Option send Alt in the recorded xterm.js on macOS, so
Alt+F opens menus.
commit
50dbc93db640cae2d16af83309029bebbefb4c45
parent
4139b6502829ea7681fdac866397bf0f20a63135
author
Dami <dami.etoile@gmail.com>
19 files changed, +515 -2
M.gitignore1+
MMakefile24++++++++-
MROADMAP.md2+-
Ademos/charts.tape34+++++++++++
Ademos/data/inventory.csv31++++++++++
Ademos/data/sales.csv17++++++
Ademos/data/spending.csv7++
Ademos/decimal.tape32++++++++++
Ademos/fakejev/main.go92++++++++++++++++++++++++++++++
Ademos/find-replace.tape31++++++++++
Ademos/first-steps.tape27+++++++++
Ademos/formats.tape31++++++++++
Ademos/freeze-sort-filter.tape48++++++++++++++++
Ademos/import.tape26++++++++
Ademos/jev.tape32++++++++++
Ademos/lib/setup.tape12++++
Ademos/lib/ttyd6++
Ademos/menus-palette.tape37++++++++++++
Ademos/selection.tape27+++++++++
M .gitignore +1 -0
@@ -6,3 +6,4 @@ e2e/testdata/screens/gallery.html
66 .claude/77 .env88 .env.*9+demos/out/
M Makefile +23 -1
@@ -7,7 +7,7 @@ GHOSTTY_SRC := $(DEPS)/ghostty-src
77 GHOSTTY_OUT := $(DEPS)/ghostty88 GHOSTTY_STAMP := $(GHOSTTY_OUT)/.built-$(GHOSTTY_COMMIT)99 10-.PHONY: build run test fuzz e2e screens oracle libghostty clean stress stress-data stress-report obs-up obs-down obs-status stress-load stress-e2e10+.PHONY: build run test fuzz e2e screens oracle demos libghostty clean stress stress-data stress-report obs-up obs-down obs-status stress-load stress-e2e1111 1212 build:1313 CGO_ENABLED=0 go build -o bin/012 ./cmd/012
@@ -70,6 +70,28 @@ stress-load:
7070 oracle:7171 cd oracle && go test -count=1 ./...7272 73+# Demo GIFs from the VHS tapes in demos/ into demos/out/ (gitignored),74+# plus smaller copies for the README in demos/out/media/ (12 fps, a75+# 64-color palette, the last frame held). Needs vhs 0.12+ (go install76+# github.com/charmbracelet/vhs@latest), ttyd and ffmpeg. The JEV demo77+# talks to demos/fakejev, started here, never the real service.78+# DEMOS=jev renders just one.79+DEMOS ?= $(basename $(notdir $(wildcard demos/*.tape)))80+FAKEJEV_ADDR := 127.0.0.1:879981+82+demos: build83+ @for t in vhs ttyd ffmpeg; do command -v $$t >/dev/null || { echo "demos need $$t (vhs: go install github.com/charmbracelet/vhs@latest; ttyd, ffmpeg: brew install ttyd ffmpeg)"; exit 1; }; done84+ CGO_ENABLED=0 go build -o bin/fakejev ./demos/fakejev85+ mkdir -p demos/out86+ bin/fakejev -addr $(FAKEJEV_ADDR) & pid=$$!; trap "kill $$pid" EXIT; \87+ export DEMOS_TTYD="$$(command -v ttyd)" PATH="$(CURDIR)/demos/lib:$$PATH"; \88+ cd demos && for d in $(DEMOS); do echo "vhs $$d.tape"; vhs -q $$d.tape || exit 1; done89+ mkdir -p demos/out/media90+ for d in $(DEMOS); do ffmpeg -v error -y -i demos/out/$$d.gif -filter_complex \91+ "fps=12,tpad=stop_mode=clone:stop_duration=2,split[a][b];[a]palettegen=max_colors=64:stats_mode=diff[p];[b][p]paletteuse=dither=none:diff_mode=rectangle" \92+ demos/out/media/$$d.gif || exit 1; done93+ @ls -l demos/out/media94+7395 libghostty: $(GHOSTTY_STAMP)7496 7597 # The commit matches the one pinned by go.mitchellh.com/libghostty.
M ROADMAP.md +1 -1
A demos/charts.tape +34 -0
A demos/data/inventory.csv +31 -0
A demos/data/sales.csv +17 -0
A demos/data/spending.csv +7 -0
A demos/decimal.tape +32 -0
A demos/fakejev/main.go +92 -0
@@ -0,0 +1,92 @@
1+// Command fakejev is a stand-in for the TypeSafe JEV service, for demo2+// recordings: it answers every question about a value by looking for3+// complaint words in the value, so a demo never needs a real key and4+// always records the same answers. Point 012 at it with5+//6+// TYPESAFE_API_KEY=demo TYPESAFE_BASE_URL=http://127.0.0.1:8799 ./bin/0127+package main8+9+import (10+ "encoding/json"11+ "flag"12+ "io"13+ "log"14+ "net/http"15+ "slices"16+ "strings"17+ "time"18+)19+20+// negative words mark a value as a complaint.21+var negative = []string{"crushed", "broken", "late", "wrong", "refund", "never", "missing"}22+23+func main() {24+ addr := flag.String("addr", "127.0.0.1:8799", "listen address")25+ delay := flag.Duration("delay", 600*time.Millisecond, "time to answer, so Loading… shows")26+ flag.Parse()27+ http.HandleFunc("/v1/systemone", func(w http.ResponseWriter, r *http.Request) {28+ body, _ := io.ReadAll(r.Body)29+ time.Sleep(*delay)30+ answer, err := answerFor(body)31+ if err != nil {32+ http.Error(w, err.Error(), http.StatusBadRequest)33+ return34+ }35+ json.NewEncoder(w).Encode(map[string]any{36+ "model": "jev-demo", "answers": map[string]any{"answer": answer},37+ "usage": map[string]int{"input_tokens": 1, "output_tokens": 1},38+ })39+ })40+ log.Printf("fake JEV service on http://%s", *addr)41+ log.Fatal(http.ListenAndServe(*addr, nil))42+}43+44+// answerFor answers the request's question: yes-or-no, a choice of45+// labels, or a score on levels.46+func answerFor(body []byte) (map[string]any, error) {47+ var req struct {48+ State json.RawMessage `json:"state"`49+ Questions map[string]struct {50+ Type string `json:"type"`51+ Criteria json.RawMessage `json:"criteria"`52+ } `json:"questions"`53+ }54+ if err := json.Unmarshal(body, &req); err != nil {55+ return nil, err56+ }57+ text := strings.ToLower(string(req.State))58+ bad := slices.ContainsFunc(negative, func(w string) bool { return strings.Contains(text, w) })59+ q := req.Questions["answer"]60+ switch q.Type {61+ case "noul":62+ p := 0.0763+ if bad {64+ p = 0.9465+ }66+ return map[string]any{"type": "noul", "noul": p}, nil67+ case "choice":68+ var labels map[string]any69+ json.Unmarshal(q.Criteria, &labels)70+ keys := make([]string, 0, len(labels))71+ for l := range labels {72+ keys = append(keys, l)73+ }74+ slices.Sort(keys)75+ pick := keys[len(keys)-1]76+ if bad {77+ pick = keys[0]78+ }79+ probs := map[string]float64{}80+ for _, l := range keys {81+ probs[l] = 0.1 / float64(max(len(keys)-1, 1))82+ }83+ probs[pick] = 0.984+ return map[string]any{"type": "choice", "choice": pick, "confidence": 0.9, "probabilities": probs}, nil85+ }86+ score, probs := 0.2, map[string]float64{"0": 0.8, "1": 0.15, "2": 0.05}87+ if bad {88+ score, probs = 1.8, map[string]float64{"0": 0.05, "1": 0.15, "2": 0.8}89+ }90+ return map[string]any{"type": "score", "score": score, "confidence": 0.8,91+ "legend": map[string]any{"0": "low", "1": "mid", "2": "high"}, "probabilities": probs}, nil92+}
A demos/find-replace.tape +31 -0
A demos/first-steps.tape +27 -0
A demos/formats.tape +31 -0
A demos/freeze-sort-filter.tape +48 -0
A demos/import.tape +26 -0
A demos/jev.tape +32 -0
A demos/lib/setup.tape +12 -0
A demos/lib/ttyd +6 -0
A demos/menus-palette.tape +37 -0
A demos/selection.tape +27 -0