Antonio Mika
·
2026-10-11
1package tui
2
3import (
4 "errors"
5 "fmt"
6 "log/slog"
7 "math"
8 "strings"
9
10 pgsdb "github.com/picosh/pico/pkg/apps/pgs/db"
11 "github.com/picosh/pico/pkg/db"
12 "github.com/picosh/pico/pkg/pssh"
13 "github.com/picosh/pico/pkg/shared"
14 "go.rockorager.dev/vaxis"
15 "go.rockorager.dev/vaxis/vxfw"
16 "go.rockorager.dev/vaxis/vxfw/richtext"
17)
18
19const (
20 adminPrefix = "admin__"
21 HOME = "dash"
22)
23
24type SharedModel struct {
25 Logger *slog.Logger
26 Session *pssh.SSHServerConnSession
27 TunsSecret string
28 Dbpool db.DB
29 PgsDB pgsdb.PgsDB
30 User *db.User
31 InvitedBy string
32 PlusFeatureFlag *db.FeatureFlag
33 BouncerFeatureFlag *db.FeatureFlag
34 Impersonator string
35 App *vxfw.App
36}
37
38type Navigate struct{ To string }
39type PageIn struct{}
40type PageOut struct{}
41
42var fuschia = vaxis.HexColor(0xEE6FF8)
43var cream = vaxis.HexColor(0xFFFDF5)
44var green = vaxis.HexColor(0x04B575)
45var grey = vaxis.HexColor(0x5C5C5C)
46var red = vaxis.HexColor(0xED567A)
47
48// var white = vaxis.HexColor(0xFFFFFF).
49var oj = vaxis.HexColor(0xFFCA80)
50var purp = vaxis.HexColor(0xBD93F9)
51var black = vaxis.HexColor(0x282A36)
52
53func createDrawCtx(ctx vxfw.DrawContext, h uint16) vxfw.DrawContext {
54 // setting some sane defaults to prevent exceptions
55 var height uint16 = 1
56 if h >= height {
57 height = h
58 }
59 var width uint16 = 80
60 if ctx.Max.Width < math.MaxUint16 {
61 width = ctx.Max.Width
62 }
63 return vxfw.DrawContext{
64 Characters: ctx.Characters,
65 Max: vxfw.Size{
66 Width: width,
67 Height: height,
68 },
69 }
70}
71
72type App struct {
73 shared *SharedModel
74 pages map[string]vxfw.Widget
75 page string
76}
77
78func (app *App) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
79 switch msg := ev.(type) {
80 case vaxis.Key:
81 if msg.Matches('c', vaxis.ModCtrl) {
82 return vxfw.QuitCmd{}, nil
83 }
84 if msg.Matches(vaxis.KeyEsc) {
85 if app.page == "signup" || app.page == HOME {
86 return nil, nil
87 }
88 app.shared.App.PostEvent(Navigate{To: HOME})
89 }
90 }
91 return nil, nil
92}
93
94type WidgetFooter interface {
95 Footer() []Shortcut
96}
97
98func (app *App) GetCurPage() vxfw.Widget {
99 return app.pages[app.page]
100}
101
102func (app *App) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
103 switch msg := ev.(type) {
104 case vxfw.Init:
105 page := HOME
106 // no user? kick them to the create account page
107 if app.shared.User == nil {
108 page = "signup"
109 }
110 app.shared.App.PostEvent(Navigate{To: page})
111 return nil, nil
112 case Navigate:
113 cmds := []vxfw.Command{}
114 cur := app.GetCurPage()
115 if cur != nil {
116 // send event to page notifying that we are leaving
117 cmd, _ := cur.(vxfw.EventHandler).HandleEvent(PageOut{}, vxfw.TargetPhase)
118 if cmd != nil {
119 cmds = append(cmds, cmd)
120 }
121 }
122
123 // switch the page
124 app.page = msg.To
125
126 next := app.GetCurPage()
127 if next != nil {
128 // send event to page notifying that we are entering
129 cmd, _ := next.(vxfw.EventHandler).HandleEvent(PageIn{}, vxfw.TargetPhase)
130 if cmd != nil {
131 cmds = append(cmds, cmd)
132 }
133 }
134
135 cmds = append(
136 cmds,
137 vxfw.RedrawCmd{},
138 )
139 return vxfw.BatchCmd(cmds), nil
140 }
141 return nil, nil
142}
143
144func (app *App) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
145 w := ctx.Max.Width
146 h := ctx.Max.Height
147 root := vxfw.NewSurface(w, ctx.Min.Height, app)
148
149 ah := 1
150 header := NewHeaderWdt(app.shared, app.page)
151 headerSurf, _ := header.Draw(vxfw.DrawContext{
152 Max: vxfw.Size{Width: w, Height: 2},
153 Characters: ctx.Characters,
154 })
155 root.AddChild(1, ah, headerSurf)
156 ah += int(headerSurf.Size.Height)
157
158 cur := app.GetCurPage()
159 if cur != nil {
160 pageCtx := vxfw.DrawContext{
161 Characters: ctx.Characters,
162 Max: vxfw.Size{Width: ctx.Max.Width - 1, Height: h - 2 - uint16(ah)},
163 }
164 surface, _ := app.GetCurPage().Draw(pageCtx)
165 root.AddChild(1, ah, surface)
166 }
167
168 wdgt, ok := cur.(WidgetFooter)
169 segs := []Shortcut{
170 {Shortcut: "^c", Text: "quit"},
171 {Shortcut: "esc", Text: "prev page"},
172 }
173 if ok {
174 segs = append(segs, wdgt.Footer()...)
175 }
176 footer := NewFooterWdt(app.shared, segs)
177 footerSurf, _ := footer.Draw(vxfw.DrawContext{
178 Max: vxfw.Size{Width: w, Height: 2},
179 Characters: ctx.Characters,
180 })
181 root.AddChild(1, int(ctx.Max.Height)-2, footerSurf)
182
183 return root, nil
184}
185
186type HeaderWdgt struct {
187 shared *SharedModel
188
189 page string
190}
191
192func NewHeaderWdt(shrd *SharedModel, page string) *HeaderWdgt {
193 return &HeaderWdgt{
194 shared: shrd,
195 page: page,
196 }
197}
198
199func (m *HeaderWdgt) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
200 return nil, nil
201}
202
203func (m *HeaderWdgt) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
204 logoTxt := "pico.sh"
205 ff := m.shared.PlusFeatureFlag
206 if ff != nil && ff.IsValid() {
207 logoTxt = "pico+"
208 }
209
210 root := vxfw.NewSurface(ctx.Max.Width, ctx.Max.Height, m)
211 // header
212 wdgt := richtext.New([]vaxis.Segment{
213 {Text: " " + logoTxt + " ", Style: vaxis.Style{Background: purp, Foreground: black}},
214 {Text: " • " + m.page, Style: vaxis.Style{Foreground: green}},
215 })
216 surf, _ := wdgt.Draw(ctx)
217 root.AddChild(0, 0, surf)
218
219 if m.shared.User != nil {
220 user := richtext.New([]vaxis.Segment{
221 {Text: "~" + m.shared.User.Name, Style: vaxis.Style{Foreground: cream}},
222 })
223 surf, _ = user.Draw(ctx)
224 root.AddChild(int(ctx.Max.Width)-int(surf.Size.Width)-1, 0, surf)
225 }
226
227 return root, nil
228}
229
230type Shortcut struct {
231 Text string
232 Shortcut string
233}
234
235type FooterWdgt struct {
236 shared *SharedModel
237
238 cmds []Shortcut
239}
240
241func NewFooterWdt(shrd *SharedModel, cmds []Shortcut) *FooterWdgt {
242 return &FooterWdgt{
243 shared: shrd,
244 cmds: cmds,
245 }
246}
247
248func (m *FooterWdgt) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
249 segs := []vaxis.Segment{}
250 for idx, shortcut := range m.cmds {
251 segs = append(
252 segs,
253 vaxis.Segment{Text: shortcut.Shortcut, Style: vaxis.Style{Foreground: fuschia}},
254 vaxis.Segment{Text: " " + shortcut.Text},
255 )
256 if idx < len(m.cmds)-1 {
257 segs = append(segs, vaxis.Segment{Text: " • "})
258 }
259 }
260 wdgt := richtext.New(segs)
261 return wdgt.Draw(ctx)
262}
263
264func initData(shrd *SharedModel) error {
265 user, err := FindUser(shrd)
266 if err != nil {
267 return err
268 }
269 shrd.User = user
270
271 ff, _ := FindFeatureFlag(shrd, "plus")
272 shrd.PlusFeatureFlag = ff
273
274 bff, _ := FindFeatureFlag(shrd, "bouncer")
275 shrd.BouncerFeatureFlag = bff
276
277 if shrd.User != nil {
278 invitedBy, _ := shrd.Dbpool.FindWhoInvitedUser(user.ID)
279 shrd.InvitedBy = invitedBy
280 }
281 return nil
282}
283
284func FindUser(shrd *SharedModel) (*db.User, error) {
285 logger := shrd.Logger
286 var user *db.User
287 usr := shrd.Session.User()
288
289 if shrd.Session.PublicKey() == nil {
290 return nil, fmt.Errorf("unable to find public key")
291 }
292
293 key := shared.KeyForKeyText(shrd.Session.PublicKey())
294
295 user, err := shrd.Dbpool.FindUserByKey(usr, key)
296 if err != nil {
297 logger.Error("no user found for public key", "err", err.Error())
298 // we only want to throw an error for specific cases
299 if errors.Is(err, &db.ErrMultiplePublicKeys{}) {
300 return nil, err
301 }
302 // no user and not error indicates we need to create an account
303 return nil, nil
304 }
305
306 origUserName := user.Name
307
308 // impersonation
309 if strings.HasPrefix(usr, adminPrefix) {
310 hasFeature := db.HasFeature(shrd.Dbpool, user.ID, "admin")
311 if hasFeature {
312 impersonate := strings.TrimPrefix(usr, adminPrefix)
313 user, err = shrd.Dbpool.FindUserByName(impersonate)
314 if err == nil {
315 shrd.Impersonator = origUserName
316 }
317 }
318 }
319
320 return user, nil
321}
322
323func FindFeatureFlag(shrd *SharedModel, name string) (*db.FeatureFlag, error) {
324 if shrd.User == nil {
325 return nil, nil
326 }
327
328 ff, err := shrd.Dbpool.FindFeature(shrd.User.ID, name)
329 if err != nil {
330 return nil, err
331 }
332
333 return ff, nil
334}
335
336func NewTui(opts vaxis.Options, shrd *SharedModel) error {
337 err := initData(shrd)
338 if err != nil {
339 return err
340 }
341
342 app, err := vxfw.NewApp(opts)
343 if err != nil {
344 return err
345 }
346
347 shrd.App = app
348 pages := map[string]vxfw.Widget{
349 HOME: NewMenuPage(shrd),
350 "pubkeys": NewPubkeysPage(shrd),
351 "add-pubkey": NewAddPubkeyPage(shrd),
352 "invite": NewAddInvitePage(shrd),
353 "tokens": NewTokensPage(shrd),
354 "add-token": NewAddTokenPage(shrd),
355 "signup": NewSignupPage(shrd),
356 "pico+": NewPlusPage(shrd),
357 "logs": NewLogsPage(shrd),
358 "analytics": NewAnalyticsPage(shrd),
359 "chat": NewChatPage(shrd),
360 "tuns": NewTunsPage(shrd),
361 "access-logs": NewAccessLogsPage(shrd),
362 "pages": NewPagesPage(shrd),
363 }
364 root := &App{
365 shared: shrd,
366 pages: pages,
367 }
368
369 return app.Run(root)
370}