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 · 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}