tangled
alpha
login
or
join now
treethought.xyz
/
attie
5
fork
atom
AT Protocol Terminal Interface Explorer
5
fork
atom
overview
issues
pulls
pipelines
better search sizing
Cam Sweeney
3 weeks ago
ad6cc0f0
6bd11701
+127
-76
3 changed files
expand all
collapse all
unified
split
at
client.go
ui
app.go
search.go
+18
-6
at/client.go
···
13
13
"github.com/bluesky-social/indigo/atproto/syntax"
14
14
)
15
15
16
16
+
// response wrappers with identity for easier navigation of views
17
17
+
18
18
+
type RepoWithIdentity struct {
19
19
+
Identity *identity.Identity
20
20
+
Repo *comatproto.RepoDescribeRepo_Output
21
21
+
}
22
22
+
23
23
+
type RecordsWithIdentity struct {
24
24
+
Identity *identity.Identity
25
25
+
Records []*agnostic.RepoListRecords_Record
26
26
+
}
27
27
+
28
28
+
type RecordWithIdentity struct {
29
29
+
Identity *identity.Identity
30
30
+
Record *agnostic.RepoGetRecord_Output
31
31
+
}
32
32
+
16
33
type Client struct {
17
34
dir identity.Directory
18
35
c *atclient.APIClient
···
43
60
log.WithFields(log.Fields{
44
61
"handle": idd.Handle,
45
62
"DID": idd.DID,
46
46
-
"PDS": idd.PDSEndpoint(),
63
63
+
"PDS": idd.PDSEndpoint(),
47
64
}).Info("identifier resolved")
48
65
return idd, nil
49
66
}
···
54
71
return nil, fmt.Errorf("failed to lookup identifier: %w", err)
55
72
}
56
73
return atclient.NewAPIClient(idd.PDSEndpoint()), nil
57
57
-
}
58
58
-
59
59
-
type RepoWithIdentity struct {
60
60
-
Identity *identity.Identity
61
61
-
Repo *comatproto.RepoDescribeRepo_Output
62
74
}
63
75
64
76
func (c *Client) GetRepo(ctx context.Context, repo string) (*RepoWithIdentity, error) {
+5
-70
ui/app.go
···
2
2
3
3
import (
4
4
"context"
5
5
-
"fmt"
6
5
7
6
log "github.com/sirupsen/logrus"
8
7
···
10
9
"github.com/bluesky-social/indigo/atproto/identity"
11
10
"github.com/bluesky-social/indigo/atproto/syntax"
12
11
"github.com/charmbracelet/bubbles/spinner"
13
13
-
"github.com/charmbracelet/bubbles/textinput"
14
12
tea "github.com/charmbracelet/bubbletea"
15
13
"github.com/treethought/goatie/at"
16
14
)
···
95
93
switch msg.String() {
96
94
case "ctrl+c", "q":
97
95
return a, tea.Quit
96
96
+
case "ctrl+k":
97
97
+
a.active = a.search
98
98
+
a.search.loading = false
99
99
+
return a, a.search.Init()
98
100
case "esc":
99
101
switch a.active {
100
102
case a.repoView:
101
103
a.active = a.search
102
104
a.search.loading = false
103
103
-
return a, nil
105
105
+
return a, a.search.Init()
104
106
case a.rlist:
105
107
a.active = a.repoView
106
108
return a, nil
···
249
251
err error
250
252
}
251
253
252
252
-
type CommandPallete struct {
253
253
-
ti textinput.Model
254
254
-
err string
255
255
-
loading bool
256
256
-
spinner spinner.Model
257
257
-
}
258
258
-
259
259
-
func (c *CommandPallete) Init() tea.Cmd {
260
260
-
c.ti = textinput.New()
261
261
-
c.ti.Placeholder = "Enter handle, DID, or AT URI"
262
262
-
c.ti.Focus()
263
263
-
c.spinner = spinner.New()
264
264
-
c.spinner.Spinner = spinner.Dot
265
265
-
return textinput.Blink
266
266
-
}
267
267
-
268
268
-
func (c *CommandPallete) SetSize(w, h int) {
269
269
-
c.ti.Width = w - 2
270
270
-
}
271
271
-
272
272
-
func (c *CommandPallete) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
273
273
-
274
274
-
switch msg := msg.(type) {
275
275
-
case tea.KeyMsg:
276
276
-
switch msg.String() {
277
277
-
case "enter":
278
278
-
val := c.ti.Value()
279
279
-
if val == "" {
280
280
-
c.err = "Input cannot be empty"
281
281
-
return c, nil
282
282
-
}
283
283
-
id, err := syntax.ParseAtIdentifier(val)
284
284
-
if err != nil {
285
285
-
c.err = fmt.Sprintf("Must use handle, DID or AT URI: %s", err.Error())
286
286
-
return c, nil
287
287
-
}
288
288
-
c.err = ""
289
289
-
c.loading = true
290
290
-
return c, func() tea.Msg {
291
291
-
log.Printf("Looking up identifier: %s", id.String())
292
292
-
return searchSubmitMsg{identifier: id}
293
293
-
}
294
294
-
}
295
295
-
296
296
-
}
297
297
-
298
298
-
var cmds []tea.Cmd
299
299
-
ti, tcmd := c.ti.Update(msg)
300
300
-
c.ti = ti
301
301
-
cmds = append(cmds, tcmd)
302
302
-
303
303
-
sp, scmd := c.spinner.Update(msg)
304
304
-
c.spinner = sp
305
305
-
cmds = append(cmds, scmd)
306
306
-
307
307
-
return c, tea.Batch(cmds...)
308
308
-
}
309
309
-
310
310
-
func (c *CommandPallete) View() string {
311
311
-
s := fmt.Sprint("Search:\n", c.ti.View())
312
312
-
if c.err != "" {
313
313
-
s += fmt.Sprintf("\nError: %s", c.err)
314
314
-
} else if c.loading {
315
315
-
s += "\nLoading... " + c.spinner.View()
316
316
-
}
317
317
-
return s
318
318
-
}
+104
ui/search.go
···
1
1
+
package ui
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
6
6
+
log "github.com/sirupsen/logrus"
7
7
+
8
8
+
"github.com/bluesky-social/indigo/atproto/syntax"
9
9
+
"github.com/charmbracelet/bubbles/spinner"
10
10
+
"github.com/charmbracelet/bubbles/textinput"
11
11
+
tea "github.com/charmbracelet/bubbletea"
12
12
+
"github.com/charmbracelet/lipgloss"
13
13
+
)
14
14
+
15
15
+
type CommandPallete struct {
16
16
+
ti textinput.Model
17
17
+
err string
18
18
+
loading bool
19
19
+
spinner spinner.Model
20
20
+
width int
21
21
+
height int
22
22
+
}
23
23
+
24
24
+
func (c *CommandPallete) Init() tea.Cmd {
25
25
+
c.ti = textinput.New()
26
26
+
c.ti.Placeholder = "Enter handle, DID, or AT URI"
27
27
+
c.ti.Focus()
28
28
+
c.ti.Width = 60
29
29
+
c.spinner = spinner.New()
30
30
+
c.spinner.Spinner = spinner.Dot
31
31
+
return textinput.Blink
32
32
+
}
33
33
+
34
34
+
func (c *CommandPallete) SetSize(w, h int) {
35
35
+
c.width = w
36
36
+
c.height = h
37
37
+
38
38
+
// Account for border (2 chars) + padding (4 chars) = 6 total
39
39
+
maxWidth := 128
40
40
+
availableWidth := w - 6
41
41
+
42
42
+
if availableWidth < maxWidth {
43
43
+
if availableWidth < 20 {
44
44
+
availableWidth = 20 // Minimum width
45
45
+
}
46
46
+
c.ti.Width = availableWidth
47
47
+
} else {
48
48
+
c.ti.Width = maxWidth
49
49
+
}
50
50
+
}
51
51
+
52
52
+
func (c *CommandPallete) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
53
53
+
54
54
+
switch msg := msg.(type) {
55
55
+
case tea.KeyMsg:
56
56
+
switch msg.String() {
57
57
+
case "enter":
58
58
+
val := c.ti.Value()
59
59
+
if val == "" {
60
60
+
c.err = "Input cannot be empty"
61
61
+
return c, nil
62
62
+
}
63
63
+
id, err := syntax.ParseAtIdentifier(val)
64
64
+
if err != nil {
65
65
+
c.err = fmt.Sprintf("Must use handle, DID or AT URI: %s", err.Error())
66
66
+
return c, nil
67
67
+
}
68
68
+
c.err = ""
69
69
+
c.loading = true
70
70
+
return c, func() tea.Msg {
71
71
+
log.Printf("Looking up identifier: %s", id.String())
72
72
+
return searchSubmitMsg{identifier: id}
73
73
+
}
74
74
+
}
75
75
+
76
76
+
}
77
77
+
78
78
+
var cmds []tea.Cmd
79
79
+
ti, tcmd := c.ti.Update(msg)
80
80
+
c.ti = ti
81
81
+
cmds = append(cmds, tcmd)
82
82
+
83
83
+
sp, scmd := c.spinner.Update(msg)
84
84
+
c.spinner = sp
85
85
+
cmds = append(cmds, scmd)
86
86
+
87
87
+
return c, tea.Batch(cmds...)
88
88
+
}
89
89
+
90
90
+
var searchStyle = lipgloss.NewStyle().Padding(1, 2).Border(lipgloss.RoundedBorder()).BorderForeground(lipgloss.Color("62"))
91
91
+
92
92
+
func (c *CommandPallete) View() string {
93
93
+
// make centered search box
94
94
+
s := c.ti.View()
95
95
+
if c.err != "" {
96
96
+
s += fmt.Sprintf("\nError: %s", c.err)
97
97
+
} else if c.loading {
98
98
+
s += "\nLoading... " + c.spinner.View()
99
99
+
}
100
100
+
101
101
+
// Center the box in the full terminal window
102
102
+
box := searchStyle.Render(s)
103
103
+
return lipgloss.Place(c.width, c.height, lipgloss.Center, lipgloss.Center, box)
104
104
+
}