Antonio Mika
·
08 Oct 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/tui/common"
11 "github.com/picosh/pico/tui/pages"
12 "github.com/picosh/utils"
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 := utils.KeyForKeyText(pk)
243
244 err = m.shared.Dbpool.InsertPublicKey(m.shared.User.ID, key, comment, nil)
245 if err != nil {
246 if errors.Is(err, db.ErrPublicKeyTaken) {
247 return KeyTakenMsg{}
248 }
249 return errMsg{err}
250 }
251
252 return KeySetMsg(m.newKey)
253 }
254}