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