Sign in

Search issue titles and bodies, change requests and comments. Private repositories you cannot read are not included.

vole
Clone

Read only, no credentials:

https://rodent.software/git/vole.git

Write over SSH with a registered key:

git clone ssh://git@0.0.0.0:2222/vole.git

A free and open-source forge that lives in your Git repo.

Code

392 commit(s) on main

Merge change request 01M70WES5ZZDJ6RP226WB11BBZ: Let people rename and remove their own passkeys keenan · 23m ago 4b8c9e1d
history blame raw 2.8 KiB
1package 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
7import (
8 "fmt"
9 "strings"
10 "time"
11)
12
13func 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
30func 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
42type crumb struct {
43 Name string
44 Path string
45}
46
47func 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).
66const 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.
71type 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
87func 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.
117func (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.
125func (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