diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-02 23:43:08 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-03 09:54:39 -0300 |
| commit | 5ec72f8723d795d0b02d7e9ebb9f555f0c7e6a2e (patch) | |
| tree | f12f4b700fff4ff6f7e818d2a542672ba0b8d76e /vrsite/page.go | |
| parent | a9263daee9413c5eff3c1f1bebcd224442fd8685 (diff) | |
| download | notevi-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.go | 348 |
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 -} |
