// vrsite — the reading room for vr trace logs. // // A vr log records who read what, where, and what they thought about it. This // program projects that log onto the jj change the code was read at: the repo // tree with per-agent coverage colors, tree-sitter highlighted sources with // coverage-tinted line numbers (colors mix where agents overlap), and notes — // both the ones agents left and the ones you write in the browser. // // It runs as a live web app (default) or writes a static export (-out). // Either way the log is the only state: notes are appended to it, and the // repository is only ever read, never modified. // // Theme comes from a Zed theme JSON; per-agent colors come from the theme's // player (collaborator) palette. package main import ( "embed" "flag" "fmt" "os" "os/user" "path/filepath" "time" ) //go:embed templates assets var tfs embed.FS func fatal(format string, a ...any) { fmt.Fprintf(os.Stderr, "vrsite: "+format+"\n", a...) os.Exit(1) } func main() { logPath := flag.String("log", "", "vr trace log (jsonl)") repo := flag.String("repo", "", "jj repo the trace was recorded in") addr := flag.String("addr", "127.0.0.1:4200", "address to serve on") who := flag.String("user", "", "name your browser notes are logged under (default: $USER)") out := flag.String("out", "", "write a static site here instead of serving") themePath := flag.String("theme", defaultTheme("dark"), "zed theme json for the dark scheme (default: discovered, else embedded One Dark)") themeName := flag.String("theme-name", "", "theme name inside the family") lightPath := flag.String("light-theme", defaultTheme("light"), "zed theme json for the light scheme (default: discovered, else embedded One Light)") lightName := flag.String("light-theme-name", "", "theme name inside the light family") title := flag.String("title", "vr trace", "site title") watch := flag.Bool("watch", false, "with -out: watch the trace log and rebuild after changes settle") debounce := flag.Duration("debounce", 10*time.Minute, "quiet period before a watched rebuild") flag.Parse() if *logPath == "" || *repo == "" { fatal("usage: vrsite -log FILE -repo DIR [-addr HOST:PORT] [-user NAME]\n" + " vrsite -log FILE -repo DIR -out DIR [-watch] [-debounce 10m]\n" + " shared: [-theme zed.json] [-theme-name N] [-light-theme f] [-title T]") } repoAbs, err := filepath.Abs(*repo) if err != nil { fatal("%v", err) } if *debounce < 0 { fatal("debounce must not be negative") } theme := loadTheme(*themePath, *themeName, "dark") light := loadTheme(*lightPath, *lightName, "light") if *out == "" { if *watch { fatal("-watch is for -out builds; the served app already follows the log") } serve(*addr, *logPath, repoAbs, *title, username(*who), theme, light) return } build := func() { buildSite(*logPath, repoAbs, *out, *title, theme, light) } build() if !*watch { return } fmt.Printf("vrsite: watching %s (rebuild after %s without changes)\n", *logPath, *debounce) watchLog(*logPath, *debounce, build) } func username(flagValue string) string { if flagValue != "" { return flagValue } if u := os.Getenv("USER"); u != "" { return u } if u, err := user.Current(); err == nil && u.Username != "" { return u.Username } return "web" } // ── watching the log (static export only) ───────────────────────────────── type logState struct { size int64 modTime time.Time } func statLog(path string) (logState, error) { info, err := os.Stat(path) if err != nil { return logState{}, err } return logState{size: info.Size(), modTime: info.ModTime()}, nil } func watchLog(path string, debounce time.Duration, rebuild func()) { watchLogUntil(path, debounce, time.Second, nil, rebuild) } func watchLogUntil(path string, debounce, poll time.Duration, done <-chan struct{}, rebuild func()) { state, err := statLog(path) if err != nil { fatal("watch %s: %v", path, err) } ticker := time.NewTicker(poll) defer ticker.Stop() var timer *time.Timer var timerC <-chan time.Time defer func() { if timer != nil { timer.Stop() } }() lastStatErr := "" for { select { case <-done: return case <-ticker.C: next, err := statLog(path) if err != nil { if msg := err.Error(); msg != lastStatErr { fmt.Fprintf(os.Stderr, "vrsite: watch %s: %v\n", path, err) lastStatErr = msg } continue } lastStatErr = "" if next == state { continue } state = next if timer == nil { timer = time.NewTimer(debounce) timerC = timer.C } else { if !timer.Stop() { select { case <-timer.C: default: } } timer.Reset(debounce) } fmt.Printf("vrsite: change detected; rebuild scheduled after %s without changes\n", debounce) case <-timerC: fmt.Println("vrsite: rebuilding after trace changes settled") timer = nil timerC = nil rebuild() } } }