summaryrefslogtreecommitdiff
path: root/vrsite/page.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/page.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/page.go')
-rw-r--r--vrsite/page.go348
1 files changed, 0 insertions, 348 deletions
diff --git a/vrsite/page.go b/vrsite/page.go
deleted file mode 100644
index 1570d45..0000000
--- a/vrsite/page.go
+++ /dev/null
@@ -1,348 +0,0 @@
-// page.go — what a template gets, and how links are formed.
-//
-// The same templates render two ways: as a static export (relative links,
-// ".html" suffixes, no server-side features) and as the live web app
-// (absolute routes, htmx-driven filters, comments and jj queries). Pages carry
-// Live so a template can ask for the difference in one place.
-package main
-
-import (
- "html/template"
- "net/url"
- "strconv"
- "strings"
-)
-
-type page struct {
- Site *Site
- Title string
- Kind string // index | file | files | notes | note | jj | change | session
- Root string // static: relative prefix; live: route prefix ending in "/"
- Live bool
- Rev string // revision being browsed ("" = the change the log points at)
- ChangeID string // resolved change id for Rev — what pages name and link
- Commit string // the commit under it — what content is read and cached at
- Default string // the change the log points at
- Path string // this request's path, so the change bar can re-aim it
- Current string // current file path, for the tree
- User string // who web comments are attributed to
-
- // what this view is scoped to. Scope is "change" (only what was recorded
- // at ChangeID) or "all"; ScopeEmpty means "change" was asked for but
- // nothing was ever recorded there, so the whole log is showing instead.
- Scope string
- ScopeEmpty bool
- Here int // notes recorded at this change
- Elsewhere int // notes recorded at any other change
- ChangeInfo *Row
-
- Tree template.HTML
- File *File
- Files []*File // every file at this change, for counts
-
- // one note on its own page — the permalink. Excerpt is the code it is
- // about, already rendered; ExErr says why there is none.
- Note *Note
- Thread *ThreadNode
- Excerpt template.HTML
- ExFrom, ExTo int
- ExErr string
-
- Notes []*Note
- Filter NoteFilter
- Facets *Facets
- Sessions []*Session
- Compact bool
- Window string
-
- JJ *JJView
- Change *Change
- Conv *Conv
- Err string
-}
-
-// JJView is one rendered jj query: what was asked, and what came back.
-type JJView struct {
- Root string
- Kind string // log | op
- Revset string
- AtOp string
- Limit int
- Rows []Row
- Err string
-}
-
-// params carries the two things every link must not lose: which change you
-// are looking at, and how wide the view is.
-func (p *page) params() string { return p.paramsWith(p.Scope) }
-
-func (p *page) paramsWith(scope string) string {
- var q []string
- // "@" is a revset like any other now that the default change is the log's,
- // so dropping it here would slide the reader off the change they picked
- if p.Rev != "" {
- q = append(q, "rev="+url.QueryEscape(p.Rev))
- }
- if scope == "all" {
- q = append(q, "scope=all")
- }
- if len(q) == 0 {
- return ""
- }
- return "?" + strings.Join(q, "&")
-}
-
-// ScopeHref is this same page seen with the other scope.
-func (p *page) ScopeHref(scope string) string {
- path := p.Path
- if path == "" {
- path = p.liveRoot()
- }
- return path + p.paramsWith(scope)
-}
-
-// BackHref is this page again at the change the log points at.
-func (p *page) BackHref() string {
- q := ""
- if p.Scope == "all" {
- q = "?scope=all"
- }
- return p.JumpPath() + q
-}
-
-// JumpPath is where the change picker submits: your own page when it is one
-// that can be read at any change, the file tree otherwise.
-func (p *page) JumpPath() string {
- switch p.Kind {
- case "index", "file", "files", "notes", "note":
- if p.Path != "" {
- return p.Path
- }
- }
- return p.liveRoot() + "files"
-}
-
-func (p *page) liveRoot() string {
- root := p.Root
- if root == "" {
- return "/"
- }
- if !strings.HasPrefix(root, "/") {
- root = "/" + root
- }
- if !strings.HasSuffix(root, "/") {
- root += "/"
- }
- return root
-}
-
-// Href maps a logical destination to a URL for this page's mode. Kinds the
-// static export has no page for return "", and templates guard on .Live.
-func (p *page) Href(kind string, arg ...string) string {
- a := ""
- if len(arg) > 0 {
- a = arg[0]
- }
- if p.Live {
- root := p.liveRoot()
- switch kind {
- case "":
- return root + p.params()
- case "notes":
- return root + "notes" + p.params()
- case "notes-list":
- return root + "notes/list" + p.params()
- case "files":
- return root + "files" + p.params()
- case "tree":
- return root + "tree" + p.params()
- case "jj":
- return root + "jj"
- case "jj-log":
- return root + "jj/log"
- case "jj-op":
- return root + "jj/op"
- case "activity":
- return root + "activity"
- case "code":
- return root + "code/" + escPath(a) + p.params()
- case "note":
- return root + "note/" + a
- case "change":
- return root + "change/" + a
- case "session":
- return root + "session/" + escPath(a)
- case "asset":
- return root + a
- }
- return root
- }
- switch kind {
- case "":
- return p.Root + "index.html"
- case "notes":
- return p.Root + "notes.html"
- case "files":
- return p.Root + "files.html"
- case "tree":
- return p.Root + "tree.html"
- case "code":
- return p.Root + "code/" + a + ".html"
- case "note":
- return p.Root + "note/" + a + ".html"
- case "asset":
- return p.Root + a
- }
- return ""
-}
-
-// codeLink is the tree's per-file linker for this page's mode.
-func (p *page) codeLink() func(string) string {
- return func(path string) string { return p.Href("code", path) }
-}
-
-func escPath(p string) string {
- parts := strings.Split(p, "/")
- for i, s := range parts {
- parts[i] = url.PathEscape(s)
- }
- return strings.Join(parts, "/")
-}
-
-// ConvHref opens the conversation a note was written in, anchored on it. It
-// needs the transcripts on this machine, so the static export has none — and
-// the conversation itself does not link back to where you already are.
-func (p *page) ConvHref(n *Note) string {
- if !p.Live || p.Kind == "session" || n.Session == "" || safeSession(n.Session) != nil {
- return ""
- }
- id := strconv.Itoa(n.ID)
- return p.Href("session", n.Session) + "?note=" + id + "#note-" + id
-}
-
-// PermaHref is the note's own page: the link to hand someone else. It is the
-// one link on the card that names nothing but the note, so it survives the
-// change scope, the filters, and the file having moved on since.
-//
-// It is empty on the note's own page, which is where it already points, and in
-// a static export of a different change, which has no page for it.
-func (p *page) PermaHref(n *Note) string {
- if p.Kind == "note" && (p.Note == nil || p.Note.ID == n.ID) {
- return ""
- }
- if !p.Live && p.Site != nil && p.Site.NoteByID(n.ID) == nil {
- return ""
- }
- return p.Href("note", strconv.Itoa(n.ID))
-}
-
-// ThreadHref opens the incoming replies and mentions for a note. The focused
-// root only needs an in-page jump; every other card gets its own permalink.
-func (p *page) ThreadHref(n *Note) string {
- if len(n.Replies) == 0 && len(n.Mentions) == 0 {
- return ""
- }
- if p.Kind == "note" && p.Note != nil && p.Note.ID == n.ID {
- return "#discussion"
- }
- h := p.Href("note", strconv.Itoa(n.ID))
- if h == "" {
- return ""
- }
- return h + "#discussion"
-}
-
-// ReplyHref is the explicit parent link. A static export may not contain the
-// parent's change, in which case it leaves the relationship as text instead of
-// manufacturing a link to a page the export did not write.
-func (p *page) ReplyHref(n *Note) string {
- if n.ReplyTo < 1 {
- return ""
- }
- if !p.Live && p.Site != nil && p.Site.NoteByID(n.ReplyTo) == nil {
- return ""
- }
- return p.Href("note", strconv.Itoa(n.ReplyTo))
-}
-
-// HasCode reports whether this view has a page for a path. The live app can
-// render any file at the change it is reading; a static export only wrote the
-// ones present at the change it holds, so a note about a file that is not
-// there must not offer a link into nothing.
-func (p *page) HasCode(path string) bool {
- if p.Live {
- return path != ""
- }
- for _, f := range p.Files {
- if f.Path == path {
- return true
- }
- }
- return false
-}
-
-// NoteHref points at the noted line, or the note list when it is unpinned.
-// The ?note= is what turns a jump into an arrival: the page opens in notes
-// focus with the card flashed and its lines marked (see app.js).
-func (p *page) NoteHref(n *Note) string {
- if n.File == "" {
- return p.Href("notes")
- }
- id := strconv.Itoa(n.ID)
- h := p.Href("code", n.File)
- sep := "?"
- if strings.Contains(h, "?") {
- sep = "&"
- }
- h += sep + "note=" + id
- if n.Start > 0 {
- h += "#L" + strconv.Itoa(n.Start)
- }
- return h
-}
-
-// newTemplates parses the page templates. The highlighter comes along because
-// note bodies are markdown, and a fenced code block in a note is painted by
-// the same grammars as the code view.
-func newTemplates(hl *highlighter) *template.Template {
- md := newMarkdown(hl)
- funcs := template.FuncMap{
- "short": short,
- "md": md.renderAt,
- "band": func(a, b uint8) bool { return b != 0 && a&b != 0 },
- "lines": func(n *Note) string {
- if n.Start <= 0 {
- return "whole file"
- }
- if n.End > n.Start {
- return ":" + strconv.Itoa(n.Start) + "-" + strconv.Itoa(n.End)
- }
- return ":" + strconv.Itoa(n.Start)
- },
- "base": func(p string) string {
- if i := strings.LastIndex(p, "/"); i >= 0 {
- return p[i+1:]
- }
- return p
- },
- "dict": func(kv ...any) map[string]any {
- m := map[string]any{}
- for i := 0; i+1 < len(kv); i += 2 {
- m[kv[i].(string)] = kv[i+1]
- }
- return m
- },
- }
- tpl, err := template.New("layout.tmpl").Funcs(funcs).ParseFS(tfs, "templates/*.tmpl")
- if err != nil {
- fatal("%v", err)
- }
- return tpl
-}
-
-func short(s string) string {
- if len(s) > 8 {
- return s[:8]
- }
- return s
-}