tangled
alpha
login
or
join now
treethought.xyz
/
attie
5
fork
atom
AT Protocol Terminal Interface Explorer
5
fork
atom
overview
issues
pulls
pipelines
use app ctx to handle navigation
Cam Sweeney
3 weeks ago
b77251c5
ad6cc0f0
+144
-48
4 changed files
expand all
collapse all
unified
split
at
client.go
ui
app.go
collection.go
record.go
+66
-18
at/client.go
···
2
2
3
3
import (
4
4
"context"
5
5
+
"encoding/json"
5
6
"fmt"
6
7
7
8
comatproto "github.com/bluesky-social/indigo/api/atproto"
···
13
14
"github.com/bluesky-social/indigo/atproto/syntax"
14
15
)
15
16
16
16
-
// response wrappers with identity for easier navigation of views
17
17
+
type Record struct {
18
18
+
Uri string
19
19
+
Cid string
20
20
+
Value *json.RawMessage
21
21
+
}
22
22
+
23
23
+
func (r *Record) Collection() string {
24
24
+
uri, err := syntax.ParseATURI(r.Uri)
25
25
+
if err != nil {
26
26
+
return ""
27
27
+
}
28
28
+
return uri.Collection().String()
29
29
+
}
30
30
+
31
31
+
func NewRecordFromList(r *agnostic.RepoListRecords_Record) *Record {
32
32
+
return &Record{
33
33
+
Uri: r.Uri,
34
34
+
Cid: r.Cid,
35
35
+
Value: r.Value,
36
36
+
}
37
37
+
}
38
38
+
39
39
+
func NewRecordFromGet(r *agnostic.RepoGetRecord_Output) *Record {
40
40
+
cid := ""
41
41
+
if r.Cid != nil {
42
42
+
cid = *r.Cid
43
43
+
}
44
44
+
return &Record{
45
45
+
Uri: r.Uri,
46
46
+
Cid: cid,
47
47
+
Value: r.Value,
48
48
+
}
49
49
+
}
17
50
18
51
type RepoWithIdentity struct {
19
52
Identity *identity.Identity
···
22
55
23
56
type RecordsWithIdentity struct {
24
57
Identity *identity.Identity
25
25
-
Records []*agnostic.RepoListRecords_Record
58
58
+
Records []*Record
59
59
+
}
60
60
+
61
61
+
func (r *RecordsWithIdentity) Collection() string {
62
62
+
if len(r.Records) == 0 {
63
63
+
return ""
64
64
+
}
65
65
+
return r.Records[0].Collection()
26
66
}
27
67
28
68
type RecordWithIdentity struct {
29
69
Identity *identity.Identity
30
30
-
Record *agnostic.RepoGetRecord_Output
70
70
+
Record *Record
31
71
}
32
72
33
73
type Client struct {
···
65
105
return idd, nil
66
106
}
67
107
68
68
-
func (c *Client) withIdentifier(ctx context.Context, raw string) (*atclient.APIClient, error) {
108
108
+
func (c *Client) withIdentifier(ctx context.Context, raw string) (*atclient.APIClient, *identity.Identity, error) {
69
109
idd, err := c.GetIdentity(ctx, raw)
70
110
if err != nil {
71
71
-
return nil, fmt.Errorf("failed to lookup identifier: %w", err)
111
111
+
return nil, nil, fmt.Errorf("failed to lookup identifier: %w", err)
72
112
}
73
73
-
return atclient.NewAPIClient(idd.PDSEndpoint()), nil
113
113
+
return atclient.NewAPIClient(idd.PDSEndpoint()), idd, nil
74
114
}
75
115
76
116
func (c *Client) GetRepo(ctx context.Context, repo string) (*RepoWithIdentity, error) {
77
77
-
id, err := c.GetIdentity(ctx, repo)
78
78
-
if err != nil {
79
79
-
return nil, fmt.Errorf("failed to lookup identifier: %w", err)
80
80
-
}
81
81
-
82
82
-
client, err := c.withIdentifier(ctx, repo)
117
117
+
client, id, err := c.withIdentifier(ctx, repo)
83
118
if err != nil {
84
119
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
85
120
}
···
102
137
}, nil
103
138
}
104
139
105
105
-
func (c *Client) ListRecords(ctx context.Context, collection, repo string) ([]*agnostic.RepoListRecords_Record, error) {
140
140
+
func (c *Client) ListRecords(ctx context.Context, collection, repo string) (*RecordsWithIdentity, error) {
106
141
log.WithFields(log.Fields{
107
142
"collection": collection,
108
143
"repo": repo,
109
144
}).Info("list records")
110
145
111
111
-
client, err := c.withIdentifier(ctx, repo)
146
146
+
client, id, err := c.withIdentifier(ctx, repo)
112
147
if err != nil {
113
148
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
114
149
}
···
117
152
if err != nil {
118
153
return nil, fmt.Errorf("failed to list records: %w", err)
119
154
}
120
120
-
return resp.Records, nil
155
155
+
156
156
+
records := make([]*Record, len(resp.Records))
157
157
+
for i, r := range resp.Records {
158
158
+
records[i] = NewRecordFromList(r)
159
159
+
}
160
160
+
161
161
+
return &RecordsWithIdentity{
162
162
+
Identity: id,
163
163
+
Records: records,
164
164
+
}, nil
121
165
}
122
166
123
123
-
func (c *Client) GetRecord(ctx context.Context, collection, repo, rkey string) (*agnostic.RepoGetRecord_Output, error) {
167
167
+
func (c *Client) GetRecord(ctx context.Context, collection, repo, rkey string) (*RecordWithIdentity, error) {
124
168
log.WithFields(log.Fields{
125
169
"collection": collection,
126
170
"repo": repo,
127
171
"rkey": rkey,
128
172
}).Info("get record")
129
173
130
130
-
client, err := c.withIdentifier(ctx, repo)
174
174
+
client, id, err := c.withIdentifier(ctx, repo)
131
175
if err != nil {
132
176
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
133
177
}
···
136
180
if err != nil {
137
181
return nil, fmt.Errorf("failed to get record: %w", err)
138
182
}
139
139
-
return resp, nil
183
183
+
184
184
+
return &RecordWithIdentity{
185
185
+
Identity: id,
186
186
+
Record: NewRecordFromGet(resp),
187
187
+
}, nil
140
188
}
+51
-17
ui/app.go
···
5
5
6
6
log "github.com/sirupsen/logrus"
7
7
8
8
-
"github.com/bluesky-social/indigo/api/agnostic"
8
8
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
9
9
"github.com/bluesky-social/indigo/atproto/identity"
10
10
"github.com/bluesky-social/indigo/atproto/syntax"
11
11
"github.com/charmbracelet/bubbles/spinner"
12
12
tea "github.com/charmbracelet/bubbletea"
13
13
"github.com/treethought/goatie/at"
14
14
)
15
15
+
16
16
+
type AppContext struct {
17
17
+
identity *identity.Identity
18
18
+
repo *comatproto.RepoDescribeRepo_Output
19
19
+
collection string
20
20
+
record *at.Record
21
21
+
}
15
22
16
23
type App struct {
17
24
client *at.Client
18
25
search *CommandPallete
19
19
-
identity identity.Identity
20
26
repoView *RepoView
21
27
rlist *RecordsList
22
28
recordView *RecordView
···
26
32
query string
27
33
spinner spinner.Model
28
34
loading bool
35
35
+
actx *AppContext
29
36
}
30
37
31
38
func NewApp(query string) *App {
···
43
50
active: search,
44
51
spinner: spin,
45
52
loading: false,
53
53
+
actx: &AppContext{},
46
54
}
47
55
}
48
56
···
53
61
return a.fetchRepo(id.String())
54
62
}
55
63
if uri, err := syntax.ParseATURI(a.query); err == nil {
56
56
-
57
64
if uri.Collection() == "" {
58
65
return a.fetchRepo(uri.Authority().String())
59
66
}
···
82
89
return tea.Batch(cmds...)
83
90
}
84
91
92
92
+
func (a *App) resetToSearch() tea.Cmd {
93
93
+
a.actx.identity = nil
94
94
+
a.actx.repo = nil
95
95
+
a.actx.collection = ""
96
96
+
a.actx.record = nil
97
97
+
a.active = a.search
98
98
+
a.loading = false
99
99
+
return a.search.Init()
100
100
+
}
101
101
+
85
102
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
86
103
switch msg := msg.(type) {
87
104
// top level always handle ctrl-c
···
100
117
case "esc":
101
118
switch a.active {
102
119
case a.repoView:
103
103
-
a.active = a.search
104
104
-
a.search.loading = false
105
105
-
return a, a.search.Init()
120
120
+
return a, a.resetToSearch()
106
121
case a.rlist:
122
122
+
if a.actx.identity == nil {
123
123
+
return a, a.resetToSearch()
124
124
+
}
125
125
+
if a.actx.repo != nil {
126
126
+
a.active = a.repoView
127
127
+
return a, a.repoView.Init()
128
128
+
}
107
129
a.active = a.repoView
108
108
-
return a, nil
130
130
+
return a, a.fetchRepo(a.actx.identity.DID.String())
109
131
case a.recordView:
132
132
+
if a.actx.collection != "" {
133
133
+
a.active = a.rlist
134
134
+
return a, a.fetchRecords(a.actx.collection, a.actx.identity.DID.String())
135
135
+
}
110
136
a.active = a.rlist
111
137
return a, nil
112
138
}
···
126
152
127
153
case repoLoadedMsg:
128
154
a.loading = false
155
155
+
a.actx.identity = msg.repo.Identity
156
156
+
a.actx.repo = msg.repo.Repo
157
157
+
a.actx.collection = ""
158
158
+
a.actx.record = nil
129
159
cmd := a.repoView.SetRepo(msg.repo)
130
160
a.repoView.SetSize(a.w, a.h) // Set size before switching view
131
161
a.active = a.repoView
···
134
164
135
165
case selectCollectionMsg:
136
166
log.Printf("Collection selected: %s", msg.collection)
167
167
+
a.actx.collection = msg.collection
137
168
return a, a.fetchRecords(msg.collection, a.repoView.repo.Handle)
138
169
139
170
case recordsLoadedMsg:
140
171
a.loading = false
141
141
-
cmd := a.rlist.SetRecords(msg.records)
172
172
+
a.actx.identity = msg.records.Identity
173
173
+
a.actx.collection = msg.records.Collection()
174
174
+
a.actx.record = nil
175
175
+
cmd := a.rlist.SetRecords(msg.records.Records)
142
176
a.rlist.SetSize(a.w, a.h) // Set size before switching view
143
177
a.active = a.rlist
144
178
a.search.loading = false
···
146
180
147
181
case recordSelectedMsg:
148
182
a.loading = false
149
149
-
a.recordView.SetRecord(msg.record)
183
183
+
a.actx.identity = msg.record.Identity
184
184
+
a.actx.collection = msg.record.Record.Collection()
185
185
+
a.actx.record = msg.record.Record
186
186
+
a.recordView.SetRecord(msg.record.Record)
150
187
a.recordView.SetSize(a.w, a.h) // Set size before switching view
151
188
a.active = a.recordView
152
189
return a, nil
···
193
230
log.WithFields(log.Fields{
194
231
"repo": repo,
195
232
"collection": collection,
196
196
-
"numRecords": len(recs),
233
233
+
"numRecords": len(recs.Records),
197
234
}).Info("Records loaded")
198
235
return recordsLoadedMsg{records: recs}
199
236
}
···
212
249
"rkey": rkey,
213
250
}).Info("Record loaded")
214
251
return recordSelectedMsg{
215
215
-
record: &agnostic.RepoListRecords_Record{
216
216
-
Uri: rec.Uri,
217
217
-
Value: rec.Value,
218
218
-
}}
252
252
+
record: rec,
253
253
+
}
219
254
}
220
255
}
221
256
···
240
275
}
241
276
242
277
type recordsLoadedMsg struct {
243
243
-
records []*agnostic.RepoListRecords_Record
278
278
+
records *at.RecordsWithIdentity
244
279
}
245
280
246
281
type recordSelectedMsg struct {
247
247
-
record *agnostic.RepoListRecords_Record
282
282
+
record *at.RecordWithIdentity
248
283
}
249
284
250
285
type repoErrorMsg struct {
251
286
err error
252
287
}
253
253
-
+23
-10
ui/collection.go
···
2
2
3
3
import (
4
4
"fmt"
5
5
+
log "github.com/sirupsen/logrus"
5
6
"strings"
6
7
7
7
-
"github.com/bluesky-social/indigo/api/agnostic"
8
8
"github.com/bluesky-social/indigo/atproto/syntax"
9
9
"github.com/charmbracelet/bubbles/list"
10
10
tea "github.com/charmbracelet/bubbletea"
11
11
"github.com/charmbracelet/lipgloss"
12
12
+
"github.com/treethought/goatie/at"
12
13
)
13
14
14
15
type RecordsList struct {
15
15
-
rlist list.Model
16
16
-
preview *RecordView
17
17
-
header string
18
18
-
w, h int
16
16
+
rlist list.Model
17
17
+
preview *RecordView
18
18
+
header string
19
19
+
w, h int
20
20
+
collection string
19
21
}
20
22
21
23
type RecordListItem struct {
22
22
-
r *agnostic.RepoListRecords_Record
24
24
+
r *at.Record
23
25
parsed syntax.ATURI
24
26
}
25
27
26
26
-
func NewRecordListItem(r *agnostic.RepoListRecords_Record) RecordListItem {
28
28
+
func NewRecordListItem(r *at.Record) RecordListItem {
27
29
uri, _ := syntax.ParseATURI(r.Uri)
28
30
return RecordListItem{
29
31
r: r,
···
49
51
return s[:half] + "..." + s[len(s)-half:]
50
52
}
51
53
52
52
-
func NewRecordsList(records []*agnostic.RepoListRecords_Record) *RecordsList {
54
54
+
func NewRecordsList(records []*at.Record) *RecordsList {
53
55
del := list.DefaultDelegate{
54
56
ShowDescription: true,
55
57
Styles: list.NewDefaultItemStyles(),
···
68
70
return rl
69
71
}
70
72
71
71
-
func (rl *RecordsList) SetRecords(records []*agnostic.RepoListRecords_Record) tea.Cmd {
73
73
+
func (rl *RecordsList) SetRecords(records []*at.Record) tea.Cmd {
74
74
+
if records == nil {
75
75
+
log.Error("SetRecords called with nil")
76
76
+
return nil
77
77
+
}
72
78
rl.preview.SetRecord(nil)
73
79
rl.rlist.SetItems(nil)
74
80
items := make([]list.Item, len(records))
···
77
83
items[i] = list.Item(ci)
78
84
}
79
85
cmd := rl.rlist.SetItems(items)
86
86
+
if len(items) > 0 {
87
87
+
rl.preview.SetRecord(items[0].(RecordListItem).r)
88
88
+
}
80
89
rl.header = rl.buildHeader()
81
90
return cmd
82
91
}
···
131
140
case "enter":
132
141
if item, ok := rl.rlist.SelectedItem().(RecordListItem); ok {
133
142
return rl, func() tea.Msg {
134
134
-
return recordSelectedMsg{record: item.r}
143
143
+
return recordSelectedMsg{
144
144
+
record: &at.RecordWithIdentity{
145
145
+
Record: item.r,
146
146
+
},
147
147
+
}
135
148
}
136
149
}
137
150
}
+4
-3
ui/record.go
···
4
4
"encoding/json"
5
5
"fmt"
6
6
7
7
-
"github.com/bluesky-social/indigo/api/agnostic"
8
7
"github.com/bluesky-social/indigo/atproto/syntax"
9
8
"github.com/charmbracelet/bubbles/viewport"
10
9
tea "github.com/charmbracelet/bubbletea"
11
10
"github.com/charmbracelet/lipgloss"
11
11
+
"github.com/treethought/goatie/at"
12
12
)
13
13
14
14
type RecordView struct {
15
15
-
record *agnostic.RepoListRecords_Record
15
15
+
record *at.Record
16
16
vp viewport.Model
17
17
header string
18
18
preview bool
···
46
46
return headerStyle.Render(header)
47
47
}
48
48
49
49
-
func (rv *RecordView) SetRecord(record *agnostic.RepoListRecords_Record) {
49
49
+
func (rv *RecordView) SetRecord(record *at.Record) {
50
50
rv.record = record
51
51
if rv.record == nil || rv.record.Value == nil {
52
52
rv.vp.SetContent("")
53
53
+
rv.header = ""
53
54
return
54
55
}
55
56
data, err := json.MarshalIndent(rv.record.Value, "", " ")