Eric Bower
·
2026-07-16
1package tui
2
3import (
4 "fmt"
5 "net"
6
7 "git.sr.ht/~rockorager/vaxis"
8 "git.sr.ht/~rockorager/vaxis/vxfw"
9 "git.sr.ht/~rockorager/vaxis/vxfw/button"
10 "git.sr.ht/~rockorager/vaxis/vxfw/richtext"
11 "git.sr.ht/~rockorager/vaxis/vxfw/text"
12 "github.com/picosh/pico/pkg/db"
13 "github.com/picosh/pico/pkg/pssh"
14 "github.com/picosh/pico/pkg/shared"
15 "golang.org/x/crypto/ssh"
16)
17
18type SignupPage struct {
19 shared *SharedModel
20 focus string
21 err error
22
23 input *TextInput
24 btn *button.Button
25}
26
27func NewSignupPage(shrd *SharedModel) *SignupPage {
28 btn := button.New("SIGNUP", func() (vxfw.Command, error) { return nil, nil })
29 btn.Style = button.StyleSet{
30 Default: vaxis.Style{Background: grey},
31 Focus: vaxis.Style{Background: oj, Foreground: black},
32 }
33 input := NewTextInput("signup")
34 return &SignupPage{shared: shrd, btn: btn, input: input}
35}
36
37func (m *SignupPage) Footer() []Shortcut {
38 return []Shortcut{
39 {Shortcut: "tab", Text: "focus"},
40 {Shortcut: "enter", Text: "create user"},
41 }
42}
43
44func (m *SignupPage) createAccount(name string) (*db.User, error) {
45 if name == "" {
46 return nil, fmt.Errorf("name cannot be empty")
47 }
48 key := shared.KeyForKeyText(m.shared.Session.PublicKey())
49 remoteAddr := m.shared.Session.Conn.RemoteAddr().String()
50 userIp, _, _ := net.SplitHostPort(remoteAddr)
51 return m.shared.Dbpool.RegisterUser(name, key, "", userIp)
52}
53
54func (m *SignupPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
55 switch msg := ev.(type) {
56 case vaxis.Key:
57 if msg.Matches(vaxis.KeyEnter) {
58 user, err := m.createAccount(m.input.GetValue())
59 if err != nil {
60 m.err = err
61 return vxfw.RedrawCmd{}, nil
62 }
63
64 m.shared.User = user
65
66 pssh.SetUser(m.shared.Session, user)
67
68 m.shared.Logger = m.shared.Logger.With(
69 "user", user.Name,
70 "userId", user.ID,
71 )
72
73 pssh.SetLogger(m.shared.Session, m.shared.Logger)
74
75 m.shared.App.PostEvent(Navigate{To: HOME})
76 }
77 }
78 return nil, nil
79}
80
81func (m *SignupPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
82 switch msg := ev.(type) {
83 case PageIn:
84 return m.input.FocusIn()
85 case vaxis.Key:
86 if msg.Matches(vaxis.KeyTab) {
87 if m.focus == "button" {
88 m.focus = "input"
89 return m.input.FocusIn()
90 }
91 m.focus = "button"
92 cmd, _ := m.input.FocusOut()
93 return vxfw.BatchCmd([]vxfw.Command{
94 cmd,
95 vxfw.FocusWidgetCmd(m.btn),
96 }), nil
97 }
98 }
99
100 return nil, nil
101}
102
103func (m *SignupPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
104 w := ctx.Max.Width
105 h := ctx.Max.Height
106
107 root := vxfw.NewSurface(w, h, m)
108
109 fp := ssh.FingerprintSHA256(m.shared.Session.PublicKey())
110 intro := richtext.New([]vaxis.Segment{
111 {Text: "Welcome to pico.sh's management TUI!\n\n"},
112 {Text: "By creating an account you get access to our pico services. We have free and paid services."},
113 {Text: " After you create an account, you can go to our docs site to get started:\n\n "},
114 {Text: "https://pico.sh/getting-started\n\n", Style: vaxis.Style{Hyperlink: "https://pico.sh/getting-started"}},
115 {Text: fmt.Sprintf("pubkey: %s\n\n", fp), Style: vaxis.Style{Foreground: purp}},
116 })
117 introSurf, _ := intro.Draw(ctx)
118 ah := 0
119 root.AddChild(0, ah, introSurf)
120 ah += int(introSurf.Size.Height)
121
122 inpSurf, _ := m.input.Draw(vxfw.DrawContext{
123 Characters: ctx.Characters,
124 Max: vxfw.Size{Width: ctx.Max.Width, Height: 4},
125 })
126 root.AddChild(0, ah, inpSurf)
127 ah += int(inpSurf.Size.Height)
128
129 btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
130 Characters: ctx.Characters,
131 Max: vxfw.Size{Width: 10, Height: 1},
132 })
133 root.AddChild(0, ah, btnSurf)
134 ah += int(btnSurf.Size.Height)
135
136 if m.err != nil {
137 errTxt := text.New(m.err.Error())
138 errTxt.Style = vaxis.Style{Foreground: red}
139 errSurf, _ := errTxt.Draw(ctx)
140 root.AddChild(0, ah, errSurf)
141 }
142
143 return root, nil
144}