vole
Clone
Read only, no credentials:
https://rodent.software/vole.git
Write over SSH with a registered key:
git clone ssh://git@rodent.software:2222/vole.git
A free and open-source forge that lives in your Git repo.
| 1 | package web |
| 2 | |
| 3 | // Presentation helpers that no single surface owns: relative time, breadcrumbs, |
| 4 | // path validation, and the page control the list pages share. Nothing here reads a store or decides anything about |
| 5 | // access; a value it returns is only ever printed. |
| 6 | |
| 7 | import ( |
| 8 | "fmt" |
| 9 | "strings" |
| 10 | "time" |
| 11 | ) |
| 12 | |
| 13 | func humanSince(t int64) string { |
| 14 | if t == 0 { |
| 15 | return "" |
| 16 | } |
| 17 | d := time.Since(time.Unix(t, 0)) |
| 18 | switch { |
| 19 | case d < time.Minute: |
| 20 | return "just now" |
| 21 | case d < time.Hour: |
| 22 | return fmt.Sprintf("%dm ago", int(d.Minutes())) |
| 23 | case d < 24*time.Hour: |
| 24 | return fmt.Sprintf("%dh ago", int(d.Hours())) |
| 25 | default: |
| 26 | return fmt.Sprintf("%dd ago", int(d.Hours()/24)) |
| 27 | } |
| 28 | } |
| 29 | |
| 30 | func safePath(p string) bool { |
| 31 | if strings.Contains(p, "\x00") { |
| 32 | return false |
| 33 | } |
| 34 | for _, seg := range strings.Split(p, "/") { |
| 35 | if seg == ".." { |
| 36 | return false |
| 37 | } |
| 38 | } |
| 39 | return true |
| 40 | } |
| 41 | |
| 42 | type crumb struct { |
| 43 | Name string |
| 44 | Path string |
| 45 | } |
| 46 | |
| 47 | func crumbs(p string) []crumb { |
| 48 | var out []crumb |
| 49 | cur := "" |
| 50 | for _, seg := range strings.Split(p, "/") { |
| 51 | if seg == "" { |
| 52 | continue |
| 53 | } |
| 54 | if cur == "" { |
| 55 | cur = seg |
| 56 | } else { |
| 57 | cur += "/" + seg |
| 58 | } |
| 59 | out = append(out, crumb{Name: seg, Path: cur}) |
| 60 | } |
| 61 | return out |
| 62 | } |
| 63 | |
| 64 | // listPageSize is how many rows one page of an issue, change-request, or run list |
| 65 | // shows. The instance activity feed has its own, larger, page size (home.go). |
| 66 | const listPageSize = 25 |
| 67 | |
| 68 | // pager is one page of a list. The handler builds the URLs, so it keeps control |
| 69 | // of which filters survive; everything a template needs to draw the control is |
| 70 | // here, including the label and the fragment a page link swaps. |
| 71 | type pager struct { |
| 72 | // Label names the nav for a screen reader ("Issue pages"). |
| 73 | Label string |
| 74 | // Target is the HTMX selector a page link replaces. Empty navigates the |
| 75 | // whole page instead. |
| 76 | Target string |
| 77 | Page int |
| 78 | Pages int |
| 79 | Total int |
| 80 | Per int |
| 81 | From int |
| 82 | To int |
| 83 | PrevURL string |
| 84 | NextURL string |
| 85 | } |
| 86 | |
| 87 | func newPager(total, per, page int, urlFor func(int) string) pager { |
| 88 | if per < 1 { |
| 89 | per = 1 |
| 90 | } |
| 91 | pages := (total + per - 1) / per |
| 92 | if pages < 1 { |
| 93 | pages = 1 |
| 94 | } |
| 95 | if page < 1 { |
| 96 | page = 1 |
| 97 | } |
| 98 | if page > pages { |
| 99 | page = pages |
| 100 | } |
| 101 | p := pager{Page: page, Pages: pages, Total: total, Per: per} |
| 102 | if total > 0 { |
| 103 | p.From = (page-1)*per + 1 |
| 104 | p.To = min(page*per, total) |
| 105 | } |
| 106 | if page > 1 { |
| 107 | p.PrevURL = urlFor(page - 1) |
| 108 | } |
| 109 | if page < pages { |
| 110 | p.NextURL = urlFor(page + 1) |
| 111 | } |
| 112 | return p |
| 113 | } |
| 114 | |
| 115 | // forSwap names the control and the fragment it replaces, for a list that pages |
| 116 | // in place through HTMX. A list without it pages by navigation. |
| 117 | func (p pager) forSwap(label, target string) pager { |
| 118 | p.Label = label |
| 119 | p.Target = target |
| 120 | return p |
| 121 | } |
| 122 | |
| 123 | // rows returns the half-open range of rows on this page, so a handler that has |
| 124 | // already collected and counted them does not repeat the arithmetic. |
| 125 | func (p pager) rows() (int, int) { |
| 126 | if p.Total == 0 { |
| 127 | return 0, 0 |
| 128 | } |
| 129 | return (p.Page - 1) * p.Per, p.To |
| 130 | } |
| 131 |