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}