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}