repos / pico

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

pico / pkg / tui
Antonio Mika  ยท  2025-04-02

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				m.confirm = false
 69				err := m.shared.Dbpool.RemoveToken(m.tokens[m.list.Cursor()].ID)
 70				if err != nil {
 71					m.err = err
 72					return nil, nil
 73				}
 74				m.err = m.fetchTokens()
 75				return vxfw.RedrawCmd{}, nil
 76			}
 77		}
 78		if msg.Matches('n') {
 79			m.confirm = false
 80			return vxfw.RedrawCmd{}, nil
 81		}
 82	}
 83
 84	return nil, nil
 85}
 86
 87func (m *TokensPage) getWidget(i uint, cursor uint) vxfw.Widget {
 88	if int(i) >= len(m.tokens) {
 89		return nil
 90	}
 91
 92	style := vaxis.Style{Foreground: grey}
 93	isSelected := i == cursor
 94	if isSelected {
 95		style = vaxis.Style{Foreground: fuschia}
 96	}
 97
 98	token := m.tokens[i]
 99	txt := richtext.New([]vaxis.Segment{
100		{Text: "Name: ", Style: style},
101		{Text: token.Name + "\n"},
102
103		{Text: "Created: ", Style: style},
104		{Text: token.CreatedAt.Format(time.DateOnly)},
105	})
106
107	return txt
108}
109
110func (m *TokensPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
111	w := ctx.Max.Width
112	h := ctx.Max.Height
113	root := vxfw.NewSurface(w, h, m)
114	ah := 0
115
116	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.")
117	brd := NewBorder(info)
118	brd.Label = "desc"
119	brdSurf, _ := brd.Draw(ctx)
120	root.AddChild(0, ah, brdSurf)
121	ah += int(brdSurf.Size.Height)
122
123	header := text.New(fmt.Sprintf("%d tokens\n\n", len(m.tokens)))
124	headerSurf, _ := header.Draw(ctx)
125	root.AddChild(0, ah, headerSurf)
126	ah += int(headerSurf.Size.Height)
127
128	listSurf, _ := m.list.Draw(ctx)
129	root.AddChild(0, ah, listSurf)
130
131	segs := []vaxis.Segment{}
132	if m.confirm {
133		segs = append(segs, vaxis.Segment{
134			Text:  "are you sure? y/n\n",
135			Style: vaxis.Style{Foreground: red},
136		})
137	}
138	if m.err != nil {
139		segs = append(segs, vaxis.Segment{
140			Text:  m.err.Error() + "\n",
141			Style: vaxis.Style{Foreground: red},
142		})
143	}
144	segs = append(segs, vaxis.Segment{Text: "\n"})
145
146	footer := richtext.New(segs)
147	footerSurf, _ := footer.Draw(createDrawCtx(ctx, 3))
148	root.AddChild(0, int(h)-3, footerSurf)
149
150	return root, nil
151}
152
153type AddTokenPage struct {
154	shared *SharedModel
155
156	token string
157	err   error
158	focus string
159	input *TextInput
160	btn   *button.Button
161}
162
163func NewAddTokenPage(shrd *SharedModel) *AddTokenPage {
164	btn := button.New("OK", func() (vxfw.Command, error) { return nil, nil })
165	btn.Style = button.StyleSet{
166		Default: vaxis.Style{Background: grey},
167		Focus:   vaxis.Style{Background: oj, Foreground: black},
168	}
169	return &AddTokenPage{
170		shared: shrd,
171
172		input: NewTextInput("enter name"),
173		btn:   btn,
174	}
175}
176
177func (m *AddTokenPage) Footer() []Shortcut {
178	return []Shortcut{
179		{Shortcut: "tab", Text: "focus"},
180		{Shortcut: "shift+click", Text: "select text"},
181		{Shortcut: "enter", Text: "add token"},
182	}
183}
184
185func (m *AddTokenPage) CaptureEvent(ev vaxis.Event) (vxfw.Command, error) {
186	switch msg := ev.(type) {
187	case vaxis.Key:
188		if msg.Matches(vaxis.KeyEnter) {
189			if m.token != "" {
190				copyToken := m.token
191				m.token = ""
192				m.err = nil
193				m.input.Reset()
194				m.shared.App.PostEvent(Navigate{To: "tokens"})
195				return vxfw.BatchCmd([]vxfw.Command{
196					vxfw.CopyToClipboardCmd(copyToken),
197					vxfw.RedrawCmd{},
198				}), nil
199			}
200			token, err := m.addToken(m.input.GetValue())
201			m.token = token
202			m.focus = "button"
203			m.err = err
204
205			return vxfw.BatchCmd([]vxfw.Command{
206				vxfw.FocusWidgetCmd(m.btn),
207				vxfw.RedrawCmd{},
208			}), err
209		}
210	}
211	return nil, nil
212}
213
214func (m *AddTokenPage) HandleEvent(ev vaxis.Event, phase vxfw.EventPhase) (vxfw.Command, error) {
215	switch msg := ev.(type) {
216	case PageIn:
217		m.focus = "input"
218		m.input.Reset()
219		return m.input.FocusIn()
220	case vaxis.Key:
221		if msg.Matches(vaxis.KeyTab) {
222			if m.focus == "input" {
223				m.focus = "button"
224				cmd, _ := m.input.FocusOut()
225				return vxfw.BatchCmd([]vxfw.Command{
226					cmd,
227					vxfw.FocusWidgetCmd(m.btn),
228				}), nil
229			}
230			m.focus = "input"
231			return m.input.FocusIn()
232		}
233	}
234
235	return nil, nil
236}
237
238func (m *AddTokenPage) addToken(name string) (string, error) {
239	return m.shared.Dbpool.InsertToken(m.shared.User.ID, name)
240}
241
242func (m *AddTokenPage) Draw(ctx vxfw.DrawContext) (vxfw.Surface, error) {
243	w := ctx.Max.Width
244	h := ctx.Max.Height
245	root := vxfw.NewSurface(w, h, m)
246	ah := 0
247
248	if m.token == "" {
249		header := text.New("Enter a name for the token")
250		headerSurf, _ := header.Draw(ctx)
251		root.AddChild(0, ah, headerSurf)
252		ah += int(headerSurf.Size.Height)
253
254		inputSurf, _ := m.input.Draw(createDrawCtx(ctx, 4))
255		root.AddChild(0, ah, inputSurf)
256		ah += int(inputSurf.Size.Height)
257
258		btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
259			Characters: ctx.Characters,
260			Max:        vxfw.Size{Width: 4, Height: 1},
261		})
262		root.AddChild(0, ah, btnSurf)
263		ah += int(btnSurf.Size.Height)
264	} else {
265		header := text.New(
266			"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",
267		)
268		headerSurf, _ := header.Draw(ctx)
269		root.AddChild(0, ah, headerSurf)
270		ah += int(headerSurf.Size.Height)
271
272		token := text.New(m.token + "\n")
273		tokenSurf, _ := token.Draw(ctx)
274		root.AddChild(0, ah, tokenSurf)
275		ah += int(tokenSurf.Size.Height)
276
277		btnSurf, _ := m.btn.Draw(vxfw.DrawContext{
278			Characters: ctx.Characters,
279			Max:        vxfw.Size{Width: 4, Height: 1},
280		})
281		root.AddChild(0, ah, btnSurf)
282		ah += int(btnSurf.Size.Height)
283	}
284
285	if m.err != nil {
286		e := text.New(m.err.Error())
287		e.Style = vaxis.Style{Foreground: red}
288		errSurf, _ := e.Draw(ctx)
289		root.AddChild(0, ah, errSurf)
290	}
291
292	return root, nil
293}