repos / pico

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

pico / tui / createtoken
Eric Bower · 17 May 24

create.go

  1package createtoken
  2
  3import (
  4	"fmt"
  5	"strings"
  6
  7	input "github.com/charmbracelet/bubbles/textinput"
  8	tea "github.com/charmbracelet/bubbletea"
  9	"github.com/picosh/pico/tui/common"
 10	"github.com/picosh/pico/tui/pages"
 11)
 12
 13type state int
 14
 15const (
 16	ready state = iota
 17	submitting
 18	submitted
 19)
 20
 21type index int
 22
 23const (
 24	textInput index = iota
 25	okButton
 26	cancelButton
 27)
 28
 29type TokenSetMsg struct {
 30	token string
 31}
 32
 33type errMsg struct {
 34	err error
 35}
 36
 37func (e errMsg) Error() string { return e.err.Error() }
 38
 39type Model struct {
 40	shared common.SharedModel
 41
 42	state     state
 43	tokenName string
 44	token     string
 45	index     index
 46	errMsg    string
 47	input     input.Model
 48}
 49
 50// NewModel returns a new username model in its initial state.
 51func NewModel(shared common.SharedModel) Model {
 52	im := input.New()
 53	im.Cursor.Style = shared.Styles.Cursor
 54	im.Placeholder = "A name used for your reference"
 55	im.PlaceholderStyle = shared.Styles.InputPlaceholder
 56	im.Prompt = shared.Styles.FocusedPrompt.String()
 57	im.CharLimit = 256
 58	im.Focus()
 59
 60	return Model{
 61		shared: shared,
 62
 63		state:     ready,
 64		tokenName: "",
 65		token:     "",
 66		index:     textInput,
 67		errMsg:    "",
 68		input:     im,
 69	}
 70}
 71
 72// updateFocus updates the focused states in the model based on the current
 73// focus index.
 74func (m *Model) updateFocus() {
 75	if m.index == textInput && !m.input.Focused() {
 76		m.input.Focus()
 77		m.input.Prompt = m.shared.Styles.FocusedPrompt.String()
 78	} else if m.index != textInput && m.input.Focused() {
 79		m.input.Blur()
 80		m.input.Prompt = m.shared.Styles.Prompt.String()
 81	}
 82}
 83
 84// Move the focus index one unit forward.
 85func (m *Model) indexForward() {
 86	m.index += 1
 87	if m.index > cancelButton {
 88		m.index = textInput
 89	}
 90
 91	m.updateFocus()
 92}
 93
 94// Move the focus index one unit backwards.
 95func (m *Model) indexBackward() {
 96	m.index -= 1
 97	if m.index < textInput {
 98		m.index = cancelButton
 99	}
100
101	m.updateFocus()
102}
103
104// Init is the Bubble Tea initialization function.
105func (m Model) Init() tea.Cmd {
106	return input.Blink
107}
108
109// Update is the Bubble Tea update loop.
110func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
111	switch msg := msg.(type) {
112	case tea.KeyMsg:
113		// Ignore keys if we're submitting
114		if m.state == submitting {
115			return m, nil
116		}
117
118		switch msg.String() {
119		case "q", "esc":
120			return m, pages.Navigate(pages.TokensPage)
121		case "tab":
122			m.indexForward()
123		case "shift+tab":
124			m.indexBackward()
125		case "l", "k", "right":
126			if m.index != textInput {
127				m.indexForward()
128			}
129		case "h", "j", "left":
130			if m.index != textInput {
131				m.indexBackward()
132			}
133		case "up", "down":
134			if m.index == textInput {
135				m.indexForward()
136			} else {
137				m.index = textInput
138				m.updateFocus()
139			}
140		case "enter":
141			switch m.index {
142			case textInput:
143				fallthrough
144			case okButton: // Submit the form
145				// form already submitted so ok button exits
146				if m.state == submitted {
147					return m, pages.Navigate(pages.TokensPage)
148				}
149
150				m.state = submitting
151				m.errMsg = ""
152				m.tokenName = strings.TrimSpace(m.input.Value())
153
154				return m, m.addToken()
155			case cancelButton:
156				return m, pages.Navigate(pages.TokensPage)
157			}
158		}
159
160		// Pass messages through to the input element if that's the element
161		// in focus
162		if m.index == textInput {
163			var cmd tea.Cmd
164			m.input, cmd = m.input.Update(msg)
165
166			return m, cmd
167		}
168
169		return m, nil
170
171	case TokenSetMsg:
172		m.state = submitted
173		m.token = msg.token
174		return m, nil
175
176	case errMsg:
177		m.state = ready
178		head := m.shared.Styles.Error.Render("Oh, what? There was a curious error we were not expecting. ")
179		body := m.shared.Styles.Subtle.Render(msg.Error())
180		m.errMsg = m.shared.Styles.Wrap.Render(head + body)
181
182		return m, nil
183
184	// leaving page so reset model
185	case pages.NavigateMsg:
186		next := NewModel(m.shared)
187		return next, next.Init()
188
189	default:
190		var cmd tea.Cmd
191		m.input, cmd = m.input.Update(msg) // Do we still need this?
192
193		return m, cmd
194	}
195}
196
197// View renders current view from the model.
198func (m Model) View() string {
199	s := "Enter a name for your token\n\n"
200	s += m.input.View() + "\n\n"
201
202	if m.state == submitting {
203		s += spinnerView()
204	} else if m.state == submitted {
205		s = fmt.Sprintf("Save this token:\n%s\n\n", m.token)
206		s += "After you exit this screen you will *not* be able to see it again.\n\n"
207		s += common.OKButtonView(m.shared.Styles, m.index == 1, true)
208	} else {
209		s += common.OKButtonView(m.shared.Styles, m.index == 1, true)
210		s += " " + common.CancelButtonView(m.shared.Styles, m.index == 2, false)
211		if m.errMsg != "" {
212			s += "\n\n" + m.errMsg
213		}
214	}
215
216	return s
217}
218
219func (m *Model) addToken() tea.Cmd {
220	return func() tea.Msg {
221		token, err := m.shared.Dbpool.InsertToken(m.shared.User.ID, m.tokenName)
222		if err != nil {
223			return errMsg{err}
224		}
225
226		return TokenSetMsg{token}
227	}
228}
229
230func spinnerView() string {
231	return "Submitting..."
232}