Antonio Mika
·
08 Oct 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 += m.shared.Styles.Label.SetString(pubkey).String()
205 s += "\n\n" + m.input.View() + "\n\n"
206
207 if m.state == submitting {
208 s += m.spinnerView()
209 } else {
210 s += common.OKButtonView(m.shared.Styles, m.index == 1, true)
211 s += " " + common.CancelButtonView(m.shared.Styles, m.index == 2, false)
212 if m.errMsg != "" {
213 s += "\n\n" + m.errMsg
214 }
215 }
216 s += fmt.Sprintf("\n\n%s\n", m.shared.Styles.HelpSection.SetString(helpMsg))
217
218 return s
219}
220
221func (m Model) spinnerView() string {
222 return "Creating account..."
223}
224
225func (m *Model) createAccount() tea.Cmd {
226 return func() tea.Msg {
227 if m.newName == "" {
228 return NameInvalidMsg{}
229 }
230
231 key := utils.KeyForKeyText(m.shared.Session.PublicKey())
232
233 user, err := m.shared.Dbpool.RegisterUser(m.newName, key, "")
234 if err != nil {
235 if errors.Is(err, db.ErrNameTaken) {
236 return NameTakenMsg{}
237 } else if errors.Is(err, db.ErrNameInvalid) {
238 return NameInvalidMsg{}
239 } else if errors.Is(err, db.ErrNameDenied) {
240 return NameInvalidMsg{}
241 } else {
242 return errMsg{err}
243 }
244 }
245
246 return CreateAccountMsg(user)
247 }
248}