repos / pico

pico services - prose.sh, pastes.sh, imgs.sh, feeds.sh, pgs.sh
git clone https://github.com/picosh/pico.git

pico / tui / createaccount
Eric Bower · 17 May 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/shared"
 12	"github.com/picosh/pico/tui/common"
 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", shared.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, err := shared.KeyForKeyText(m.shared.Session.PublicKey())
232		if err != nil {
233			return errMsg{err}
234		}
235
236		user, err := m.shared.Dbpool.RegisterUser(m.newName, key, "")
237		if err != nil {
238			if errors.Is(err, db.ErrNameTaken) {
239				return NameTakenMsg{}
240			} else if errors.Is(err, db.ErrNameInvalid) {
241				return NameInvalidMsg{}
242			} else if errors.Is(err, db.ErrNameDenied) {
243				return NameInvalidMsg{}
244			} else {
245				return errMsg{err}
246			}
247		}
248
249		return CreateAccountMsg(user)
250	}
251}