Eric Bower
·
15 Dec 24
create.go
1package createaccount
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7
8 input "github.com/charmbracelet/bubbles/textinput"
9 tea "github.com/charmbracelet/bubbletea"
10 "github.com/picosh/pico/db"
11 "github.com/picosh/pico/tui/common"
12 "github.com/picosh/utils"
13)
14
15type state int
16
17const (
18 ready state = iota
19 submitting
20)
21
22// index specifies the UI element that's in focus.
23type index int
24
25const (
26 textInput index = iota
27 okButton
28 cancelButton
29)
30
31type CreateAccountMsg *db.User
32
33// NameTakenMsg is sent when the requested username has already been taken.
34type NameTakenMsg struct{}
35
36// NameInvalidMsg is sent when the requested username has failed validation.
37type NameInvalidMsg struct{}
38
39type errMsg struct{ err error }
40
41func (e errMsg) Error() string { return e.err.Error() }
42
43var deny = strings.Join(db.DenyList, ", ")
44var helpMsg = fmt.Sprintf("Names can only contain plain letters and numbers and must be less than 50 characters. No emjois. No names from deny list: %s", deny)
45
46// Model holds the state of the username UI.
47type Model struct {
48 shared *common.SharedModel
49
50 state state
51 newName string
52 index index
53 errMsg string
54 input input.Model
55}
56
57// NewModel returns a new username model in its initial state.
58func NewModel(shared *common.SharedModel) Model {
59 im := input.New()
60 im.Cursor.Style = shared.Styles.Cursor
61 im.Placeholder = "enter username"
62 im.PlaceholderStyle = shared.Styles.InputPlaceholder
63 im.Prompt = shared.Styles.FocusedPrompt.String()
64 im.CharLimit = 50
65 im.Focus()
66
67 return Model{
68 shared: shared,
69 state: ready,
70 newName: "",
71 index: textInput,
72 errMsg: "",
73 input: im,
74 }
75}
76
77// updateFocus updates the focused states in the model based on the current
78// focus index.
79func (m *Model) updateFocus() {
80 if m.index == textInput && !m.input.Focused() {
81 m.input.Focus()
82 m.input.Prompt = m.shared.Styles.FocusedPrompt.String()
83 } else if m.index != textInput && m.input.Focused() {
84 m.input.Blur()
85 m.input.Prompt = m.shared.Styles.Prompt.String()
86 }
87}
88
89// Move the focus index one unit forward.
90func (m *Model) indexForward() {
91 m.index++
92 if m.index > cancelButton {
93 m.index = textInput
94 }
95
96 m.updateFocus()
97}
98
99// Move the focus index one unit backwards.
100func (m *Model) indexBackward() {
101 m.index--
102 if m.index < textInput {
103 m.index = cancelButton
104 }
105
106 m.updateFocus()
107}
108
109// Init is the Bubble Tea initialization function.
110func (m Model) Init() tea.Cmd {
111 return input.Blink
112}
113
114// Update is the Bubble Tea update loop.
115func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
116 switch msg := msg.(type) {
117 case tea.KeyMsg:
118 // Ignore keys if we're submitting
119 if m.state == submitting {
120 return m, nil
121 }
122
123 switch msg.String() {
124 case "tab":
125 m.indexForward()
126 case "shift+tab":
127 m.indexBackward()
128 case "l", "k", "right":
129 if m.index != textInput {
130 m.indexForward()
131 }
132 case "h", "j", "left":
133 if m.index != textInput {
134 m.indexBackward()
135 }
136 case "up", "down":
137 if m.index == textInput {
138 m.indexForward()
139 } else {
140 m.index = textInput
141 m.updateFocus()
142 }
143 case "enter":
144 switch m.index {
145 case textInput:
146 fallthrough
147 case okButton: // Submit the form
148 m.state = submitting
149 m.errMsg = ""
150 m.newName = strings.TrimSpace(m.input.Value())
151
152 return m, m.createAccount()
153 case cancelButton: // Exit
154 return m, tea.Quit
155 }
156 }
157
158 // Pass messages through to the input element if that's the element
159 // in focus
160 if m.index == textInput {
161 var cmd tea.Cmd
162 m.input, cmd = m.input.Update(msg)
163
164 return m, cmd
165 }
166
167 return m, nil
168
169 case NameTakenMsg:
170 m.state = ready
171 m.errMsg = m.shared.Styles.Subtle.Render("Sorry, ") +
172 m.shared.Styles.Error.Render(m.newName) +
173 m.shared.Styles.Subtle.Render(" is taken.")
174
175 return m, nil
176
177 case NameInvalidMsg:
178 m.state = ready
179 head := m.shared.Styles.Error.Render("Invalid name.")
180 m.errMsg = m.shared.Styles.Wrap.Render(head)
181
182 return m, nil
183
184 case errMsg:
185 m.state = ready
186 head := m.shared.Styles.Error.Render("Oh, what? There was a curious error we were not expecting. ")
187 body := m.shared.Styles.Subtle.Render(msg.Error())
188 m.errMsg = m.shared.Styles.Wrap.Render(head + body)
189
190 return m, nil
191
192 default:
193 var cmd tea.Cmd
194 m.input, cmd = m.input.Update(msg) // Do we still need this?
195
196 return m, cmd
197 }
198}
199
200// View renders current view from the model.
201func (m Model) View() string {
202 s := common.LogoView() + "\n\n"
203 pubkey := fmt.Sprintf("pubkey: %s", utils.KeyForSha256(m.shared.Session.PublicKey()))
204 s += "\nWelcome to pico.sh's management TUI. By creating an account you get access to our pico services. We have free and paid services. After you create an account, you can go to the Settings page to see which services you can access.\n\n"
205 s += m.shared.Styles.Label.SetString(pubkey).String()
206 s += "\n\n" + m.input.View() + "\n\n"
207
208 if m.state == submitting {
209 s += m.spinnerView()
210 } else {
211 s += common.OKButtonView(m.shared.Styles, m.index == 1, true)
212 s += " " + common.CancelButtonView(m.shared.Styles, m.index == 2, false)
213 if m.errMsg != "" {
214 s += "\n\n" + m.errMsg
215 }
216 }
217 s += fmt.Sprintf("\n\n%s\n", m.shared.Styles.HelpSection.SetString(helpMsg))
218
219 return s
220}
221
222func (m Model) spinnerView() string {
223 return "Creating account..."
224}
225
226func (m *Model) createAccount() tea.Cmd {
227 return func() tea.Msg {
228 if m.newName == "" {
229 return NameInvalidMsg{}
230 }
231
232 key := utils.KeyForKeyText(m.shared.Session.PublicKey())
233
234 user, err := m.shared.Dbpool.RegisterUser(m.newName, key, "")
235 if err != nil {
236 if errors.Is(err, db.ErrNameTaken) {
237 return NameTakenMsg{}
238 } else if errors.Is(err, db.ErrNameInvalid) {
239 return NameInvalidMsg{}
240 } else if errors.Is(err, db.ErrNameDenied) {
241 return NameInvalidMsg{}
242 } else {
243 return errMsg{err}
244 }
245 }
246
247 return CreateAccountMsg(user)
248 }
249}