- 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
+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=
+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"
+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 }
+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 }
+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 }
+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))
+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
+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