repos / pico

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

pico / tui / createkey
Eric Bower · 17 May 24

create.go

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