repos / pico

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

pico / pkg / tui
Eric Bower  ·  2026-01-02

pages.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"sort"
  6	"strings"
  7
  8	"git.sr.ht/~rockorager/vaxis"
  9	"git.sr.ht/~rockorager/vaxis/vxfw"
 10	"git.sr.ht/~rockorager/vaxis/vxfw/list"
 11	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 12	"git.sr.ht/~rockorager/vaxis/vxfw/text"
 13	"github.com/picosh/pico/pkg/db"
 14)
 15
 16type PagesLoaded struct{}
 17
 18type PagesPage struct {
 19	shared *SharedModel
 20
 21	list    *list.Dynamic
 22	pages   []*db.Project
 23	loading bool
 24	err     error
 25}
 26
 27func NewPagesPage(shrd *SharedModel) *PagesPage {
 28	page := &PagesPage{
 29		shared: shrd,
 30	}
 31	page.list = &list.Dynamic{Builder: page.getWidget, DrawCursor: true, Gap: 1}
 32	return page
 33}
 34
 35func (m *PagesPage) Footer() []Shortcut {
 36	return []Shortcut{
 37		{Shortcut: "c", Text: "copy url"},
 38		{Shortcut: "^r", Text: "refresh"},
 39	}
 40}
 41
 42func (m *PagesPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 43	switch msg := ev.(type) {
 44	case PageIn:
 45		m.loading = true
 46		go m.fetchPages()
 47		return vxfw.FocusWidgetCmd(m.list), nil
 48	case PagesLoaded:
 49		return vxfw.RedrawCmd{}, nil
 50	case vaxis.Key:
 51		if msg.Matches('c') {
 52			cursor := m.list.Cursor()
 53			if int(cursor) < len(m.pages) {
 54				project := m.pages[cursor]
 55				url := fmt.Sprintf("https://%s-%s.pgs.sh", m.shared.User.Name, project.Name)
 56				return vxfw.CopyToClipboardCmd(url), nil
 57			}
 58		}
 59		if msg.Matches('r', vaxis.ModCtrl) {
 60			m.loading = true
 61			go m.fetchPages()
 62			return vxfw.RedrawCmd{}, nil
 63		}
 64	}
 65	return nil, nil
 66}
 67
 68func (m *PagesPage) fetchPages() {
 69	if m.shared.User == nil {
 70		m.err = fmt.Errorf("no user found")
 71		m.loading = false
 72		m.shared.App.PostEvent(PagesLoaded{})
 73		return
 74	}
 75
 76	if m.shared.PgsDB == nil {
 77		m.err = fmt.Errorf("pgs database not configured")
 78		m.loading = false
 79		m.shared.App.PostEvent(PagesLoaded{})
 80		return
 81	}
 82
 83	pages, err := m.shared.PgsDB.FindProjectsByUser(m.shared.User.ID)
 84
 85	m.loading = false
 86	if err != nil {
 87		m.err = err
 88		m.pages = []*db.Project{}
 89	} else {
 90		m.err = nil
 91		sort.Slice(pages, func(i, j int) bool {
 92			return pages[i].Name < pages[j].Name
 93		})
 94		m.pages = pages
 95	}
 96
 97	m.shared.App.PostEvent(PagesLoaded{})
 98}
 99
100func (m *PagesPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
101	w := ctx.Max.Width
102	h := ctx.Max.Height
103	root := vxfw.NewSurface(w, h, m)
104	ah := 0
105
106	pagesLen := len(m.pages)
107	err := m.err
108
109	info := text.New("Sites deployed to pgs.  Each page is accessible via https://{user}-{project}.pgs.sh.  We do not have access to custom domains so those cannot be shown.")
110	brd := NewBorder(info)
111	brd.Label = "desc"
112	brdSurf, _ := brd.Draw(createDrawCtx(ctx, 5))
113	root.AddChild(0, ah, brdSurf)
114	ah += int(brdSurf.Size.Height)
115
116	if err != nil {
117		txt := text.New(fmt.Sprintf("Error: %s", err.Error()))
118		txt.Style = vaxis.Style{Foreground: red}
119		txtSurf, _ := txt.Draw(ctx)
120		root.AddChild(0, ah, txtSurf)
121	} else if pagesLen == 0 {
122		txt := text.New("No pages found.")
123		txtSurf, _ := txt.Draw(ctx)
124		root.AddChild(0, ah, txtSurf)
125	} else {
126		listPane := NewBorder(m.list)
127		listPane.Label = "pages"
128		listPane.Style = vaxis.Style{Foreground: oj}
129		listSurf, _ := listPane.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)))
130		root.AddChild(0, ah, listSurf)
131	}
132
133	return root, nil
134}
135
136func (m *PagesPage) getWidget(i uint, cursor uint) vxfw.Widget {
137	if int(i) >= len(m.pages) {
138		return nil
139	}
140
141	isSelected := i == cursor
142	return pageToWidget(m.pages[i], m.shared.User.Name, isSelected)
143}
144
145func pageToWidget(project *db.Project, username string, isSelected bool) vxfw.Widget {
146	url := fmt.Sprintf("https://%s-%s.pgs.sh", username, project.Name)
147
148	updatedAt := ""
149	if project.UpdatedAt != nil {
150		updatedAt = project.UpdatedAt.Format("2006-01-02 15:04:05")
151	}
152
153	labelStyle := vaxis.Style{Foreground: grey}
154	if isSelected {
155		labelStyle = vaxis.Style{Foreground: fuschia}
156	}
157
158	segs := []vaxis.Segment{
159		{Text: "URL: ", Style: labelStyle},
160		{Text: url + "\n", Style: vaxis.Style{Foreground: green}},
161
162		{Text: "Updated: ", Style: labelStyle},
163		{Text: updatedAt},
164	}
165
166	if project.ProjectDir != project.Name {
167		segs = append(segs,
168			vaxis.Segment{Text: "\n"},
169			vaxis.Segment{Text: "Links To: ", Style: labelStyle},
170			vaxis.Segment{Text: project.ProjectDir, Style: vaxis.Style{Foreground: purp}},
171		)
172	}
173
174	if project.Acl.Type != "" && project.Acl.Type != "public" {
175		aclStr := project.Acl.Type
176		if len(project.Acl.Data) > 0 {
177			aclStr += " (" + strings.Join(project.Acl.Data, ", ") + ")"
178		}
179		segs = append(segs,
180			vaxis.Segment{Text: "\n"},
181			vaxis.Segment{Text: "ACL: ", Style: labelStyle},
182			vaxis.Segment{Text: aclStr, Style: vaxis.Style{Foreground: oj}},
183		)
184	}
185
186	if project.Blocked != "" {
187		segs = append(segs,
188			vaxis.Segment{Text: "\n"},
189			vaxis.Segment{Text: "Blocked: ", Style: labelStyle},
190			vaxis.Segment{Text: project.Blocked, Style: vaxis.Style{Foreground: red}},
191		)
192	}
193
194	txt := richtext.New(segs)
195	return txt
196}