repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

pico / pkg / tui
Eric Bower  ·  2025-05-05

tokens.go

  1package tui
  2
  3import (
  4	"fmt"
  5	"time"
  6
  7	"git.sr.ht/~rockorager/vaxis"
  8	"git.sr.ht/~rockorager/vaxis/vxfw"
  9	"git.sr.ht/~rockorager/vaxis/vxfw/button"
 10	"git.sr.ht/~rockorager/vaxis/vxfw/list"
 11	"git.sr.ht/~rockorager/vaxis/vxfw/richtext"
 12	"git.sr.ht/~rockorager/vaxis/vxfw/text"
 13	"github.com/picosh/pico/pkg/db"
 14)
 15
 16type TokensPage struct {
 17	shared *SharedModel
 18	list   list.Dynamic
 19
 20	tokens  []*db.Token
 21	err     error
 22	confirm bool
 23}
 24
 25func NewTokensPage(shrd *SharedModel) *TokensPage {
 26	m := &TokensPage{
 27		shared: shrd,
 28	}
 29	m.list = list.Dynamic{DrawCursor: true, Builder: m.getWidget, Gap: 1}
 30	return m
 31}
 32
 33type FetchTokens struct{}
 34
 35func (m *TokensPage) Footer() []Shortcut {
 36	return []Shortcut{
 37		{Shortcut: "j/k", Text: "choose"},
 38		{Shortcut: "x", Text: "delete"},
 39		{Shortcut: "c", Text: "create"},
 40	}
 41}
 42
 43func (m *TokensPage) fetchTokens() error {
 44	tokens, err := m.shared.Dbpool.FindTokensForUser(m.shared.User.ID)
 45	if err != nil {
 46		return err
 47
 48	}
 49	m.tokens = tokens
 50	return nil
 51}
 52
 53func (m *TokensPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
 54	switch msg := ev.(type) {
 55	case PageIn:
 56		m.err = m.fetchTokens()
 57		return vxfw.FocusWidgetCmd(&m.list), nil
 58	case vaxis.Key:
 59		if msg.Matches('c') {
 60			m.shared.App.PostEvent(Navigate{To: "add-token"})
 61		}
 62		if msg.Matches('x') {
 63			m.confirm = true
 64			return vxfw.RedrawCmd{}, nil
 65		}
 66		if msg.Matches('y') {
 67			if m.confirm {
 68				cursor := int(m.list.Cursor())
 69				if cursor >= len(m.tokens) {
 70					return nil, nil
 71				}
 72				m.confirm = false
 73				err := m.shared.Dbpool.RemoveToken(m.tokens[m.list.Cursor()].ID)
 74				if err != nil {
 75					m.err = err
 76					return nil, nil
 77				}
 78				m.err = m.fetchTokens()
 79				return vxfw.RedrawCmd{}, nil
 80			}
 81		}
 82		if msg.Matches('n') {
 83			m.confirm = false
 84			return vxfw.RedrawCmd{}, nil
 85		}
 86	}
 87
 88	return nil, nil
 89}
 90
 91func (m *TokensPage) getWidget(i uint, cursor uint) vxfw.Widget {
 92	if int(i) >= len(m.tokens) {
 93		return nil
 94	}
 95
 96	style := vaxis.Style{Foreground: grey}
 97	isSelected := i == cursor
 98	if isSelected {
 99		style = vaxis.Style{Foreground: fuschia}
100	}
101
102	token := m.tokens[i]
103	txt := richtext.New([]vaxis.Segment{
104		{Text: "Name: ", Style: style},
105		{Text: token.Name + "\n"},
106
107		{Text: "Created: ", Style: style},
108		{Text: token.CreatedAt.Format(time.DateOnly)},
109	})
110
111	return txt
112}
113
114func (m *TokensPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
115	w := ctx.Max.Width
116	h := ctx.Max.Height
117	root := vxfw.NewSurface(w, h, m)
118	ah := 0
119
120	info := text.New("Tokens allows users to generate a 'password' for use with web services that cannot use SSH keys for authentication. For example, tokens are used to access our IRC bouncer.")
121	brd := NewBorder(info)
122	brd.Label = "desc"
123	brdSurf, _ := brd.Draw(ctx)
124	root.AddChild(0, ah, brdSurf)
125	ah += int(brdSurf.Size.Height)
126
127	header := text.New(fmt.Sprintf("%d tokens\n\n", len(m.tokens)))
128	headerSurf, _ := header.Draw(ctx)
129	root.AddChild(0, ah, headerSurf)
130	ah += int(headerSurf.Size.Height)
131
132	footerHeight := 3
133	listSurf, _ := m.list.Draw(createDrawCtx(ctx, ctx.Max.Height-uint16(ah)-uint16(footerHeight)))
134	root.AddChild(0, ah, listSurf)
135
136	segs := []vaxis.Segment{}
137	if m.confirm {
138		segs = append(segs, vaxis.Segment{
139			Text:  "are you sure? y/n\n",
140			Style: vaxis.Style{Foreground: red},
141		})
142	}
143	if m.err != nil {
144		segs = append(segs, vaxis.Segment{
145			Text:  m.err.Error() + "\n",
146			Style: vaxis.Style{Foreground: red},
147		})
148	}
149	segs = append(segs, vaxis.Segment{Text: "\n"})
150
151	footer := richtext.New(segs)
152	footerSurf, _ := footer.Draw(createDrawCtx(ctx, uint16(footerHeight)))
153	root.AddChild(0, int(h)-footerHeight, footerSurf)
154
155	return root, nil
156}
157
158type AddTokenPage struct {
159	shared *SharedModel
160
161	token string
162	err   error
163	focus string
164	input *TextInput
165	btn   *button.Button
166}
167
168func NewAddTokenPage(shrd *SharedModel) *AddTokenPage {
169	btn := button.New("OK", func() (vxfw.Command, error) { return nil, nil })
170	btn.Style = button.StyleSet{
171		Default: vaxis.Style{Background: grey},
172		Focus:   vaxis.Style{Background: oj, Foreground: black},
173	}
174	return &AddTokenPage{
175		shared: shrd,
176
177		input: NewTextInput("enter name"),
178		btn:   btn,
179	}
180}
181
182func (m *AddTokenPage) Footer() []Shortcut {
183	return []Shortcut{
184		{Shortcut: "tab", Text: "focus"},
185		{Shortcut: "shift+click", Text: "select text"},
186		{Shortcut: "enter", Text: "add token"},
187	}
188}
189
190func (m *AddTokenPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
191	switch msg := ev.(type) {
192	case vaxis.Key:
193		if msg.Matches(vaxis.KeyEnter) {
194			if m.token != "" {
195				copyToken := m.token
196				m.token = ""
197				m.err = nil
198				m.input.Reset()
199				m.shared.App.PostEvent(Navigate{To: "tokens"})
200				return vxfw.BatchCmd([]vxfw.Command{
201					vxfw.CopyToClipboardCmd(copyToken),
202					vxfw.RedrawCmd{},
203				}), nil
204			}
205			token, err := m.addToken(m.input.GetValue())
206			m.token = token
207			m.focus = "button"
208			m.err = err
209
210			return vxfw.BatchCmd([]vxfw.Command{
211				vxfw.FocusWidgetCmd(m.btn),
212				vxfw.RedrawCmd{},
213			}), err
214		}
215	}
216	return nil, nil
217}
218
219func (m *AddTokenPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
220	switch msg := ev.(type) {
221	case PageIn:
222		m.focus = "input"
223		m.input.Reset()
224		return m.input.FocusIn()
225	case vaxis.Key:
226		if msg.Matches(vaxis.KeyTab) {
227			if m.focus == "input" {
228				m.focus = "button"
229				cmd, _ := m.input.FocusOut()
230				return vxfw.BatchCmd([]vxfw.Command{
231					cmd,
232					vxfw.FocusWidgetCmd(m.btn),
233				}), nil
234			}
235			m.focus = "input"
236			return m.input.FocusIn()
237		}
238	}
239
240	return nil, nil
241}
242
243func (m *AddTokenPage) addToken(name string) (string, error) {
244	return m.shared.Dbpool.InsertToken(m.shared.User.ID, name)
245}
246
247func (m *AddTokenPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
248	w := ctx.Max.Width
249	h := ctx.Max.Height
250	root := vxfw.NewSurface(w, h, m)
251	ah := 0
252
253	if m.token == "" {
254		header := text.New("Enter a name for the token")
255		headerSurf, _ := header.Draw(ctx)
256		root.AddChild(0, ah, headerSurf)
257		ah += int(headerSurf.Size.Height)
258
259		inputSurf, _ := m.input.Draw(createDrawCtx(ctx, 4))
260		root.AddChild(0, ah, inputSurf)
261		ah += int(inputSurf.Size.Height)
262
263		btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
264			Characters: ctx.Characters,
265			Max:        vxfw.Size{Width: 4, Height: 1},
266		})
267		root.AddChild(0, ah, btnSurf)
268		ah += int(btnSurf.Size.Height)
269	} else {
270		header := text.New(
271			"After you exit this screen you will *not* be able to see it again.  Use shift+click to select and copy text. If your terminal supports OSC52 then we will copy to your host clipboard upon exit of this screen.\n\n",
272		)
273		headerSurf, _ := header.Draw(ctx)
274		root.AddChild(0, ah, headerSurf)
275		ah += int(headerSurf.Size.Height)
276
277		token := text.New(m.token + "\n")
278		tokenSurf, _ := token.Draw(ctx)
279		root.AddChild(0, ah, tokenSurf)
280		ah += int(tokenSurf.Size.Height)
281
282		btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
283			Characters: ctx.Characters,
284			Max:        vxfw.Size{Width: 4, Height: 1},
285		})
286		root.AddChild(0, ah, btnSurf)
287		ah += int(btnSurf.Size.Height)
288	}
289
290	if m.err != nil {
291		e := text.New(m.err.Error())
292		e.Style = vaxis.Style{Foreground: red}
293		errSurf, _ := e.Draw(ctx)
294		root.AddChild(0, ah, errSurf)
295	}
296
297	return root, nil
298}