main pico / pkg / tui / info.go
Eric Bower  ·  2026-08-24
  1package tui
  2
  3import (
  4	"fmt"
  5	"time"
  6
  7	"git.sr.ht/~rockorager/vaxis"
  8	"git.sr.ht/~rockorager/vaxis/vxfw"
  9	"github.com/picosh/pico/pkg/db"
 10)
 11
 12type UsageInfo struct {
 13	stats *db.UserServiceStats
 14	Label string
 15}
 16
 17func NewUsageInfo(label string, stats *db.UserServiceStats) *UsageInfo {
 18	return &UsageInfo{Label: label, stats: stats}
 19}
 20
 21func (m *UsageInfo) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 22	return nil, nil
 23}
 24
 25func (m *UsageInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 26	info := NewKv(m.getKv())
 27	brd := NewBorder(info)
 28	brd.Label = m.Label
 29	brd.Style = vaxis.Style{Foreground: purp}
 30	return brd.Draw(vxfw.DrawContext{
 31		Characters: ctx.Characters,
 32		Max: vxfw.Size{
 33			Width:  30,
 34			Height: 3 + 3,
 35		},
 36	})
 37}
 38
 39func (m *UsageInfo) getKv() []Kv {
 40	label := "posts"
 41	if m.Label == "pages" {
 42		label = "sites"
 43	}
 44	kv := []Kv{
 45		{Key: label, Value: fmt.Sprintf("%d", m.stats.Num)},
 46		{Key: "oldest", Value: m.stats.FirstCreatedAt.Format(time.DateOnly)},
 47		{Key: "newest", Value: m.stats.LastestCreatedAt.Format(time.DateOnly)},
 48	}
 49	return kv
 50}
 51
 52type UserInfo struct {
 53	shared *SharedModel
 54}
 55
 56func NewUserInfo(shrd *SharedModel) *UserInfo {
 57	return &UserInfo{shrd}
 58}
 59
 60func (m *UserInfo) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 61	return nil, nil
 62}
 63
 64func (m *UserInfo) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
 65	features := NewKv(m.getKv())
 66	brd := NewBorder(features)
 67	brd.Label = "info"
 68	brd.Style = vaxis.Style{Foreground: purp}
 69	h := 1
 70	if m.shared.PlusFeatureFlag != nil {
 71		h += 1
 72	}
 73	return brd.Draw(vxfw.DrawContext{
 74		Characters: ctx.Characters,
 75		Max: vxfw.Size{
 76			Width:  30,
 77			Height: uint16(h) + 3,
 78		},
 79	})
 80}
 81
 82func (m *UserInfo) getKv() []Kv {
 83	createdAt := m.shared.User.CreatedAt.Format(time.DateOnly)
 84	kv := []Kv{
 85		{Key: "joined", Value: createdAt},
 86	}
 87
 88	if m.shared.PlusFeatureFlag != nil {
 89		expiresAt := m.shared.PlusFeatureFlag.ExpiresAt.Format(time.DateOnly)
 90		kv = append(kv, Kv{Key: "pico+ expires", Value: expiresAt})
 91	}
 92
 93	return kv
 94}
 95
 96type FeaturesList struct {
 97	shared   *SharedModel
 98	features []*db.FeatureFlag
 99	err      error
100}
101
102func NewFeaturesList(shrd *SharedModel) *FeaturesList {
103	return &FeaturesList{shared: shrd}
104}
105
106func (m *FeaturesList) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
107	switch ev.(type) {
108	case vxfw.Init:
109		m.err = m.fetchFeatures()
110		return vxfw.RedrawCmd{}, nil
111	}
112	return nil, nil
113}
114
115func (m *FeaturesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
116	features := NewKv(m.getFeaturesKv())
117	brd := NewBorder(features)
118	brd.Label = "features"
119	brd.Style = vaxis.Style{Foreground: purp}
120	return brd.Draw(vxfw.DrawContext{
121		Characters: ctx.Characters,
122		Max: vxfw.Size{
123			Width:  30,
124			Height: uint16(len(m.features)) + 4,
125		},
126	})
127}
128
129func (m *FeaturesList) fetchFeatures() error {
130	features, err := m.shared.Dbpool.FindFeaturesByUser(m.shared.User.ID)
131	m.features = features
132	return err
133}
134
135func (m *FeaturesList) getFeaturesKv() []Kv {
136	kv := []Kv{
137		{
138			Key:   "name",
139			Value: "expires at",
140			Style: vaxis.Style{
141				UnderlineColor: purp,
142				UnderlineStyle: vaxis.UnderlineDashed,
143				Foreground:     purp,
144			},
145		},
146	}
147
148	for _, feature := range m.features {
149		kv = append(kv, Kv{Key: feature.Name, Value: feature.ExpiresAt.Format(time.DateOnly)})
150	}
151
152	return kv
153}
154
155type ServicesList struct {
156	plusFf *db.FeatureFlag
157	pgsFf  *db.FeatureFlag
158}
159
160func NewServicesList(plusFf *db.FeatureFlag, pgsFf *db.FeatureFlag) *ServicesList {
161	return &ServicesList{plusFf: plusFf, pgsFf: pgsFf}
162}
163
164func (m *ServicesList) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
165	return nil, nil
166}
167
168func (m *ServicesList) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
169	services := NewKv(m.getServiceKv())
170	brd := NewBorder(services)
171	brd.Label = "services"
172	brd.Style = vaxis.Style{Foreground: purp}
173	servicesHeight := 8
174	return brd.Draw(vxfw.DrawContext{
175		Characters: ctx.Characters,
176		Max: vxfw.Size{
177			Width:  30,
178			Height: uint16(servicesHeight) + 3,
179		},
180	})
181}
182
183func (m *ServicesList) getServiceKv() []Kv {
184	hasPlus := m.plusFf != nil && m.plusFf.IsValid()
185	hasPgs := m.pgsFf != nil && m.pgsFf.IsValid()
186	pagesStatus := "pico+"
187	if hasPlus || hasPgs {
188		pagesStatus = "active"
189	}
190	data := [][]string{
191		{"name", "status"},
192		{"prose", "active"},
193		{"pipe", "active"},
194		{"pastes", "active"},
195		{"pages", pagesStatus},
196	}
197
198	if hasPlus {
199		data = append(
200			data,
201			[]string{"tuns", "active"},
202			[]string{"irc bouncer", "active"},
203			[]string{"rss-to-email", "active"},
204		)
205	} else {
206		data = append(
207			data,
208			[]string{"tuns", "pico+"},
209			[]string{"irc bouncer", "pico+"},
210			[]string{"rss-to-email", "pico+"},
211		)
212	}
213
214	kv := []Kv{}
215	for idx, d := range data {
216		value := d[1]
217		var style vaxis.Style
218		if idx == 0 {
219			style = vaxis.Style{
220				UnderlineColor: purp,
221				UnderlineStyle: vaxis.UnderlineDashed,
222				Foreground:     purp,
223			}
224		} else if value == "active" {
225			style = vaxis.Style{Foreground: green}
226		} else if value == "free tier" {
227			style = vaxis.Style{Foreground: oj}
228		} else {
229			style = vaxis.Style{Foreground: red}
230		}
231
232		kv = append(kv, Kv{
233			Key:   d[0],
234			Value: value,
235			Style: style,
236		})
237
238	}
239
240	return kv
241}