repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
8367b92
parent
c46187d
author
Eric Bower
date
2025-03-17 12:04:09 -0400 EDT
style(tui): analytics tweaks
9 files changed,  +148, -126
M go.mod
M go.sum
M go.mod
+1, -1
1@@ -22,7 +22,7 @@ toolchain go1.24.0
2 
3 require (
4 	git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0
5-	git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83
6+	git.sr.ht/~rockorager/vaxis v0.13.0
7 	github.com/alecthomas/chroma/v2 v2.15.0
8 	github.com/antoniomika/syncmap v1.0.0
9 	github.com/araddon/dateparse v0.0.0-20210429162001-6b43995a97de
M go.sum
+2, -0
1@@ -33,6 +33,8 @@ git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0 h1:Knm2mHQwLsh1sv
2 git.sr.ht/~delthas/senpai v0.3.1-0.20250311003540-18f699aaf9b0/go.mod h1:RzVz1R7QRHGcRDnJTcr7AN/cD3rj9scdgvupkXTJLYk=
3 git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83 h1:9eVqJxJzMdnpfqfKKjvEvNDpVg6sIBvbI4FdTjhHqx8=
4 git.sr.ht/~rockorager/vaxis v0.12.1-0.20250312161844-81636f76af83/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
5+git.sr.ht/~rockorager/vaxis v0.13.0 h1:+F3ze1t4X5x87QEsyn/b9b9pWXw9MDqoHynoR/PHqKg=
6+git.sr.ht/~rockorager/vaxis v0.13.0/go.mod h1:h94aKek3frIV1hJbdXjqnBqaLkbWXvV+UxAsQHg9bns=
7 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96 h1:cTp8I5+VIoKjsnZuH8vjyaysT/ses3EvZeaV/1UkF2M=
8 github.com/AndreasBriese/bbloom v0.0.0-20190825152654-46b345b51c96/go.mod h1:bOvUY6CB00SOBii9/FifXqc0awNKxLFCL/+pkDPuyl8=
9 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
M pkg/tui/analytics.go
+41, -18
 1@@ -216,7 +216,7 @@ func (m *AnalyticsPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 2 			},
 3 		}
 4 
 5-		detailSurf, _ := m.detail(rightCtx).Draw(rightCtx)
 6+		detailSurf, _ := m.detail(rightCtx, data.Intervals).Draw(rightCtx)
 7 		rightSurf.AddChild(0, ah, detailSurf)
 8 		ah += int(detailSurf.Size.Height)
 9 
10@@ -255,18 +255,29 @@ func (m *AnalyticsPage) getSiteData() (*db.SummaryVisits, error) {
11 	return val, nil
12 }
13 
14-func (m *AnalyticsPage) detail(ctx vxfw.DrawContext) vxfw.Widget {
15-	datestr := "Date range: "
16+func (m *AnalyticsPage) detail(ctx vxfw.DrawContext, visits []*db.VisitInterval) vxfw.Widget {
17+	datestr := ""
18 	now := time.Now()
19 	if m.interval == "day" {
20-		datestr += now.Format("2006 Jan")
21+		datestr += now.Format("2006 Jan") + " by day"
22 	} else {
23-		datestr += now.Format("2006")
24+		datestr += now.Format("2006") + " by month"
25 	}
26-	txt := richtext.New([]vaxis.Segment{
27-		{Text: datestr, Style: vaxis.Style{Foreground: green}},
28-	})
29-	rightPane := NewBorder(txt)
30+	kv := []Kv{
31+		{Key: "date range", Value: datestr, Style: vaxis.Style{Foreground: green}},
32+	}
33+	sum := 0
34+	for _, data := range visits {
35+		sum += data.Visitors
36+	}
37+	avg := 0
38+	if len(visits) > 0 {
39+		avg = sum / len(visits)
40+	}
41+
42+	kv = append(kv, Kv{Key: "avg req/period", Value: fmt.Sprintf("%d", avg)})
43+
44+	rightPane := NewBorder(NewKv(kv))
45 	rightPane.Width = ctx.Max.Width
46 	rightPane.Label = m.selected
47 	m.focusBorder(rightPane)
48@@ -274,11 +285,16 @@ func (m *AnalyticsPage) detail(ctx vxfw.DrawContext) vxfw.Widget {
49 }
50 
51 func (m *AnalyticsPage) urls(ctx vxfw.DrawContext, urls []*db.VisitUrl, label string) vxfw.Widget {
52-	segs := []vaxis.Segment{}
53+	kv := []Kv{}
54+	w := 15
55 	for _, url := range urls {
56-		segs = append(segs, vaxis.Segment{Text: fmt.Sprintf("%s: %d\n", url.Url, url.Count)})
57+		if len(url.Url) > w {
58+			w = len(url.Url)
59+		}
60+		kv = append(kv, Kv{Key: url.Url, Value: fmt.Sprintf("%d", url.Count)})
61 	}
62-	wdgt := richtext.New(segs)
63+	wdgt := NewKv(kv)
64+	wdgt.KeyColWidth = w + 1
65 	rightPane := NewBorder(wdgt)
66 	rightPane.Width = ctx.Max.Width
67 	rightPane.Label = label
68@@ -287,16 +303,23 @@ func (m *AnalyticsPage) urls(ctx vxfw.DrawContext, urls []*db.VisitUrl, label st
69 }
70 
71 func (m *AnalyticsPage) visits(ctx vxfw.DrawContext, intervals []*db.VisitInterval) vxfw.Widget {
72-	segs := []vaxis.Segment{}
73+	kv := []Kv{}
74+	w := 0
75 	for _, visit := range intervals {
76-		segs = append(
77-			segs,
78-			vaxis.Segment{
79-				Text: fmt.Sprintf("%s: %d\n", visit.Interval.Format(time.RFC3339), visit.Visitors),
80+		key := visit.Interval.Format(time.DateOnly)
81+		if len(key) > w {
82+			w = len(key)
83+		}
84+		kv = append(
85+			kv,
86+			Kv{
87+				Key:   key,
88+				Value: fmt.Sprintf("%d", visit.Visitors),
89 			},
90 		)
91 	}
92-	wdgt := richtext.New(segs)
93+	wdgt := NewKv(kv)
94+	wdgt.KeyColWidth = w + 1
95 	rightPane := NewBorder(wdgt)
96 	rightPane.Width = ctx.Max.Width
97 	rightPane.Label = "visits"
M pkg/tui/group.go
+7, -2
 1@@ -26,15 +26,20 @@ func (m *GroupStack) HandleEvent(vaxis.Event, vxfw.EventPhase) (vxfw.Command, er
 2 func (m *GroupStack) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 3 	root := vxfw.NewSurface(ctx.Max.Width, ctx.Max.Height, m)
 4 	ah := 0
 5+	aw := 0
 6 	for _, surf := range m.s {
 7 		if m.Direction == "vertical" {
 8 			root.AddChild(0, ah, surf)
 9 			ah += int(surf.Size.Height) + m.Gap
10 		} else {
11 			// horizontal
12-			root.AddChild(ah, 0, surf)
13-			ah += int(surf.Size.Width) + m.Gap
14+			root.AddChild(aw, 0, surf)
15+			if surf.Size.Height > uint16(ah) {
16+				ah = int(surf.Size.Height)
17+			}
18+			aw += int(surf.Size.Width) + m.Gap
19 		}
20 	}
21+	root.Size.Height = uint16(ah)
22 	return root, nil
23 }
M pkg/tui/info.go
+62, -70
  1@@ -6,7 +6,6 @@ import (
  2 
  3 	"git.sr.ht/~rockorager/vaxis"
  4 	"git.sr.ht/~rockorager/vaxis/vxfw"
  5-	"git.sr.ht/~rockorager/vaxis/vxfw/text"
  6 	"github.com/picosh/pico/pkg/db"
  7 )
  8 
  9@@ -24,7 +23,7 @@ func (m *UsageInfo) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Com
 10 }
 11 
 12 func (m *UsageInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 13-	info := NewKv(m.getKv)
 14+	info := NewKv(m.getKv())
 15 	brd := NewBorder(info)
 16 	brd.Label = m.Label
 17 	brd.Style = vaxis.Style{Foreground: purp}
 18@@ -37,20 +36,17 @@ func (m *UsageInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 19 	})
 20 }
 21 
 22-func (m *UsageInfo) getKv(idx uint16) (vxfw.Widget, vxfw.Widget) {
 23-	if int(idx) >= 3 {
 24-		return nil, nil
 25-	}
 26+func (m *UsageInfo) getKv() []Kv {
 27 	label := "posts"
 28 	if m.Label == "pages" {
 29 		label = "sites"
 30 	}
 31-	kv := [][]string{
 32-		{label, fmt.Sprintf("%d", m.stats.Num)},
 33-		{"oldest", m.stats.FirstCreatedAt.Format(time.DateOnly)},
 34-		{"newest", m.stats.LastestCreatedAt.Format(time.DateOnly)},
 35+	kv := []Kv{
 36+		{Key: label, Value: fmt.Sprintf("%d", m.stats.Num)},
 37+		{Key: "oldest", Value: m.stats.FirstCreatedAt.Format(time.DateOnly)},
 38+		{Key: "newest", Value: m.stats.LastestCreatedAt.Format(time.DateOnly)},
 39 	}
 40-	return text.New(kv[idx][0]), text.New(kv[idx][1])
 41+	return kv
 42 }
 43 
 44 type UserInfo struct {
 45@@ -66,7 +62,7 @@ func (m *UserInfo) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Comm
 46 }
 47 
 48 func (m *UserInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 49-	features := NewKv(m.getKv)
 50+	features := NewKv(m.getKv())
 51 	brd := NewBorder(features)
 52 	brd.Label = "info"
 53 	brd.Style = vaxis.Style{Foreground: purp}
 54@@ -83,22 +79,18 @@ func (m *UserInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 55 	})
 56 }
 57 
 58-func (m *UserInfo) getKv(idx uint16) (vxfw.Widget, vxfw.Widget) {
 59-	if int(idx) >= 2 {
 60-		return nil, nil
 61-	}
 62-
 63+func (m *UserInfo) getKv() []Kv {
 64 	createdAt := m.shared.User.CreatedAt.Format(time.DateOnly)
 65-	if idx == 0 {
 66-		return text.New("joined"), text.New(createdAt)
 67+	kv := []Kv{
 68+		{Key: "joined", Value: createdAt},
 69 	}
 70 
 71 	if m.shared.PlusFeatureFlag != nil {
 72 		expiresAt := m.shared.PlusFeatureFlag.ExpiresAt.Format(time.DateOnly)
 73-		return text.New("pico+ expires"), text.New(expiresAt)
 74+		kv = append(kv, Kv{Key: "pico+ expires", Value: expiresAt})
 75 	}
 76 
 77-	return nil, nil
 78+	return kv
 79 }
 80 
 81 type FeaturesList struct {
 82@@ -121,7 +113,7 @@ func (m *FeaturesList) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.
 83 }
 84 
 85 func (m *FeaturesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 86-	features := NewKv(m.getFeaturesKv)
 87+	features := NewKv(m.getFeaturesKv())
 88 	brd := NewBorder(features)
 89 	brd.Label = "features"
 90 	brd.Style = vaxis.Style{Foreground: purp}
 91@@ -140,28 +132,24 @@ func (m *FeaturesList) fetchFeatures() error {
 92 	return err
 93 }
 94 
 95-func (m *FeaturesList) getFeaturesKv(idx uint16) (vxfw.Widget, vxfw.Widget) {
 96-	kv := [][]string{
 97-		{"name", "expires at"},
 98-	}
 99-	for _, feature := range m.features {
100-		kv = append(kv, []string{feature.Name, feature.ExpiresAt.Format(time.DateOnly)})
101-	}
102-
103-	if int(idx) >= len(kv) {
104-		return nil, nil
105+func (m *FeaturesList) getFeaturesKv() []Kv {
106+	kv := []Kv{
107+		{
108+			Key:   "name",
109+			Value: "expires at",
110+			Style: vaxis.Style{
111+				UnderlineColor: purp,
112+				UnderlineStyle: vaxis.UnderlineDashed,
113+				Foreground:     purp,
114+			},
115+		},
116 	}
117 
118-	key := text.New(kv[idx][0])
119-	value := text.New(kv[idx][1])
120-
121-	if idx == 0 {
122-		style := vaxis.Style{UnderlineColor: purp, UnderlineStyle: vaxis.UnderlineDashed, Foreground: purp}
123-		key.Style = style
124-		value.Style = style
125+	for _, feature := range m.features {
126+		kv = append(kv, Kv{Key: feature.Name, Value: feature.ExpiresAt.Format(time.DateOnly)})
127 	}
128 
129-	return key, value
130+	return kv
131 }
132 
133 type ServicesList struct {
134@@ -177,7 +165,7 @@ func (m *ServicesList) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.
135 }
136 
137 func (m *ServicesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
138-	services := NewKv(m.getServiceKv)
139+	services := NewKv(m.getServiceKv())
140 	brd := NewBorder(services)
141 	brd.Label = "services"
142 	brd.Style = vaxis.Style{Foreground: purp}
143@@ -191,9 +179,9 @@ func (m *ServicesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
144 	})
145 }
146 
147-func (m *ServicesList) getServiceKv(idx uint16) (vxfw.Widget, vxfw.Widget) {
148+func (m *ServicesList) getServiceKv() []Kv {
149 	hasPlus := m.ff != nil
150-	kv := [][]string{
151+	data := [][]string{
152 		{"name", "status"},
153 		{"prose", "active"},
154 		{"pipe", "active"},
155@@ -202,42 +190,46 @@ func (m *ServicesList) getServiceKv(idx uint16) (vxfw.Widget, vxfw.Widget) {
156 	}
157 
158 	if hasPlus {
159-		kv = append(
160-			kv,
161+		data = append(
162+			data,
163 			[]string{"pages", "active"},
164 			[]string{"tuns", "active"},
165 			[]string{"irc bouncer", "active"},
166 		)
167 	} else {
168-		kv = append(
169-			kv,
170+		data = append(
171+			data,
172 			[]string{"pages", "free tier"},
173 			[]string{"tuns", "pico+"},
174 			[]string{"irc bouncer", "pico+"},
175 		)
176 	}
177 
178-	if int(idx) >= len(kv) {
179-		return nil, nil
180-	}
181-
182-	key := text.New(kv[idx][0])
183-	value := text.New(kv[idx][1])
184-	val := kv[idx][1]
185-
186-	if val == "active" {
187-		value.Style = vaxis.Style{Foreground: green}
188-	} else if val == "free tier" {
189-		value.Style = vaxis.Style{Foreground: oj}
190-	} else {
191-		value.Style = vaxis.Style{Foreground: red}
192-	}
193-
194-	if idx == 0 {
195-		style := vaxis.Style{UnderlineColor: purp, UnderlineStyle: vaxis.UnderlineDashed, Foreground: purp}
196-		key.Style = style
197-		value.Style = style
198-	}
199-
200-	return key, value
201+	kv := []Kv{}
202+	for idx, d := range data {
203+		value := d[1]
204+		var style vaxis.Style
205+		if idx == 0 {
206+			style = vaxis.Style{
207+				UnderlineColor: purp,
208+				UnderlineStyle: vaxis.UnderlineDashed,
209+				Foreground:     purp,
210+			}
211+		} else if value == "active" {
212+			style = vaxis.Style{Foreground: green}
213+		} else if value == "free tier" {
214+			style = vaxis.Style{Foreground: oj}
215+		} else {
216+			style = vaxis.Style{Foreground: red}
217+		}
218+
219+		kv = append(kv, Kv{
220+			Key:   d[0],
221+			Value: value,
222+			Style: style,
223+		})
224+
225+	}
226+
227+	return kv
228 }
M pkg/tui/kv.go
+18, -8
 1@@ -3,18 +3,25 @@ package tui
 2 import (
 3 	"git.sr.ht/~rockorager/vaxis"
 4 	"git.sr.ht/~rockorager/vaxis/vxfw"
 5+	"git.sr.ht/~rockorager/vaxis/vxfw/text"
 6 )
 7 
 8 type KVBuilder func(uint16) (vxfw.Widget, vxfw.Widget)
 9 
10+type Kv struct {
11+	Key   string
12+	Value string
13+	Style vaxis.Style
14+}
15+
16 type KvData struct {
17-	Builder     KVBuilder
18+	Data        []Kv
19 	KeyColWidth int
20 }
21 
22-func NewKv(builder KVBuilder) *KvData {
23+func NewKv(data []Kv) *KvData {
24 	return &KvData{
25-		Builder:     builder,
26+		Data:        data,
27 		KeyColWidth: 15,
28 	}
29 }
30@@ -31,11 +38,11 @@ func (m *KvData) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
31 
32 	ah := 0
33 	var idx uint16 = 0
34-	for {
35-		key, value := m.Builder(idx)
36-		if key == nil {
37-			break
38-		}
39+	for _, data := range m.Data {
40+		key := text.New(data.Key)
41+		key.Style = data.Style
42+		value := text.New(data.Value)
43+		value.Style = data.Style
44 		lft, _ := key.Draw(ctx)
45 		left.AddChild(0, ah, lft)
46 		rht, _ := value.Draw(ctx)
47@@ -46,5 +53,8 @@ func (m *KvData) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
48 	root.AddChild(0, 0, left)
49 	root.AddChild(lng, 0, right)
50 
51+	root.Size.Width = left.Size.Width + right.Size.Width - 2
52+	root.Size.Height = uint16(ah)
53+
54 	return root, nil
55 }
M pkg/tui/plus.go
+15, -25
 1@@ -53,7 +53,7 @@ func (m *PlusPage) header(ctx vxfw.DrawContext) vxfw.Surface {
 2 		{Text: "• prose\n"},
 3 		{Text: "  • blog analytics\n"},
 4 		{Text: "• irc bouncer\n"},
 5-		{Text: "• 10GB total storage\n\n"},
 6+		{Text: "• 10GB total storage\n"},
 7 	})
 8 	brd := NewBorder(intro)
 9 	brd.Label = "pico+"
10@@ -61,18 +61,6 @@ func (m *PlusPage) header(ctx vxfw.DrawContext) vxfw.Surface {
11 	return surf
12 }
13 
14-func (m *PlusPage) contact(ctx vxfw.DrawContext) vxfw.Surface {
15-	intro := richtext.New([]vaxis.Segment{
16-		{Text: "Have any questions? Feel free to reach out:\n\n"},
17-		{Text: "• "}, {Text: "mailto:hello@pico.sh\n", Style: vaxis.Style{Hyperlink: "mailto:hello@pico.sh"}},
18-		{Text: "• "}, {Text: "https://pico.sh/irc\n", Style: vaxis.Style{Hyperlink: "https://pico.sh/irc"}},
19-	})
20-	brd := NewBorder(intro)
21-	brd.Label = "contact"
22-	surf, _ := brd.Draw(ctx)
23-	return surf
24-}
25-
26 func (m *PlusPage) payment(ctx vxfw.DrawContext) vxfw.Surface {
27 	paymentLink := "https://auth.pico.sh/checkout"
28 	link := fmt.Sprintf("%s/%s", paymentLink, url.QueryEscape(m.shared.User.Name))
29@@ -91,11 +79,17 @@ func (m *PlusPage) payment(ctx vxfw.DrawContext) vxfw.Surface {
30 		{Text: "Send cash (USD) or check to:\n\n"},
31 		{Text: "• pico.sh LLC\n"},
32 		{Text: "• 206 E Huron St\n"},
33-		{Text: "• Ann Arbor MI 48104\n"},
34+		{Text: "• Ann Arbor MI 48104\n\n"},
35+		{Text: "Have any questions? Feel free to reach out:\n\n"},
36+		{Text: "• "}, {Text: "mailto:hello@pico.sh\n", Style: vaxis.Style{Hyperlink: "mailto:hello@pico.sh"}},
37+		{Text: "• "}, {Text: "https://pico.sh/irc\n", Style: vaxis.Style{Hyperlink: "https://pico.sh/irc"}},
38 	})
39 	brd := NewBorder(pay)
40 	brd.Label = "payment"
41-	surf, _ := brd.Draw(ctx)
42+	surf, _ := brd.Draw(vxfw.DrawContext{
43+		Characters: ctx.Characters,
44+		Max:        vxfw.Size{Width: 50, Height: math.MaxUint16},
45+	})
46 	return surf
47 }
48 
49@@ -106,22 +100,18 @@ func (m *PlusPage) notes(ctx vxfw.DrawContext) vxfw.Surface {
50 	})
51 	brd := NewBorder(wdgt)
52 	brd.Label = "notes"
53-	surf, _ := brd.Draw(ctx)
54+	surf, _ := brd.Draw(vxfw.DrawContext{
55+		Characters: ctx.Characters,
56+		Max:        vxfw.Size{Width: 50, Height: math.MaxUint16},
57+	})
58 	return surf
59 }
60 
61 func (m *PlusPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
62-	hdr := NewGroupStack([]vxfw.Surface{
63-		m.header(ctx),
64-		m.contact(ctx),
65-	})
66-	hdr.Direction = "horizontal"
67-	hdr.Gap = 1
68-	hdrSurf, _ := hdr.Draw(createDrawCtx(ctx, 14))
69 	stack := NewGroupStack([]vxfw.Surface{
70-		hdrSurf,
71-		m.payment(ctx),
72+		m.header(ctx),
73 		m.notes(ctx),
74+		m.payment(ctx),
75 	})
76 	stack.Gap = 1
77 	surf, _ := stack.Draw(createDrawCtx(ctx, math.MaxUint16))
M pkg/tui/pubkeys.go
+1, -1
1@@ -28,7 +28,7 @@ func NewPubkeysPage(shrd *SharedModel) *PubkeysPage {
2 	m := &PubkeysPage{
3 		shared: shrd,
4 	}
5-	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget}
6+	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget, Gap: 1}
7 	return m
8 }
9 
M pkg/tui/tokens.go
+1, -1
1@@ -26,7 +26,7 @@ func NewTokensPage(shrd *SharedModel) *TokensPage {
2 	m := &TokensPage{
3 		shared: shrd,
4 	}
5-	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget}
6+	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget, Gap: 1}
7 	return m
8 }
9