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}