tangled
alpha
login
or
join now
treethought.xyz
/
attie
5
fork
atom
AT Protocol Terminal Interface Explorer
5
fork
atom
overview
issues
pulls
pipelines
initial commit
Cam Sweeney
3 weeks ago
6e6f65e8
+581
7 changed files
expand all
collapse all
unified
split
.gitignore
at
client.go
go.mod
go.sum
main.go
ui
app.go
repo.go
+1
.gitignore
···
1
1
+
debug.log
+92
at/client.go
···
1
1
+
package at
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"fmt"
6
6
+
7
7
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
8
8
+
log "github.com/sirupsen/logrus"
9
9
+
10
10
+
"github.com/bluesky-social/indigo/api/agnostic"
11
11
+
"github.com/bluesky-social/indigo/atproto/atclient"
12
12
+
"github.com/bluesky-social/indigo/atproto/identity"
13
13
+
"github.com/bluesky-social/indigo/atproto/syntax"
14
14
+
)
15
15
+
16
16
+
type Client struct {
17
17
+
dir identity.Directory
18
18
+
c *atclient.APIClient
19
19
+
}
20
20
+
21
21
+
func NewClient(service string) *Client {
22
22
+
dir := &identity.BaseDirectory{}
23
23
+
if service == "" {
24
24
+
service = "https://bsky.social"
25
25
+
}
26
26
+
client := atclient.NewAPIClient(service)
27
27
+
return &Client{
28
28
+
dir: dir,
29
29
+
c: client,
30
30
+
}
31
31
+
}
32
32
+
33
33
+
func (c *Client) withIdentifier(ctx context.Context, id syntax.AtIdentifier) (*atclient.APIClient, error) {
34
34
+
idd, err := c.dir.Lookup(ctx, id)
35
35
+
if err != nil {
36
36
+
return nil, fmt.Errorf("failed to lookup identifier: %w", err)
37
37
+
}
38
38
+
log.WithFields(log.Fields{
39
39
+
"handle": idd.Handle,
40
40
+
"DID": idd.DID,
41
41
+
}).Info("identifier resolved")
42
42
+
return c.c.WithService(idd.PDSEndpoint()), nil
43
43
+
}
44
44
+
45
45
+
func (c *Client) GetRepo(ctx context.Context, repo string) (*comatproto.RepoDescribeRepo_Output, error) {
46
46
+
log.WithFields(log.Fields{
47
47
+
"repo": repo,
48
48
+
}).Info("describe repo")
49
49
+
50
50
+
ri, err := syntax.ParseAtIdentifier(repo)
51
51
+
if err != nil {
52
52
+
return nil, fmt.Errorf("failed to parse repo identifier: %w", err)
53
53
+
}
54
54
+
55
55
+
client, err := c.withIdentifier(ctx, ri)
56
56
+
if err != nil {
57
57
+
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
58
58
+
}
59
59
+
60
60
+
// TODO: downlaod repo as car
61
61
+
// https://github.com/bluesky-social/cookbook/blob/main/go-repo-export/main.go#L46
62
62
+
resp, err := comatproto.RepoDescribeRepo(ctx, client, repo)
63
63
+
if err != nil {
64
64
+
return nil, fmt.Errorf("failed to describe repo: %w", err)
65
65
+
}
66
66
+
return resp, nil
67
67
+
68
68
+
}
69
69
+
70
70
+
func (c *Client) GetRecord(ctx context.Context, collection, repo, rkey string) (*agnostic.RepoGetRecord_Output, error) {
71
71
+
log.WithFields(log.Fields{
72
72
+
"collection": collection,
73
73
+
"repo": repo,
74
74
+
"rkey": rkey,
75
75
+
}).Info("get record")
76
76
+
77
77
+
ri, err := syntax.ParseAtIdentifier(repo)
78
78
+
if err != nil {
79
79
+
return nil, fmt.Errorf("failed to parse repo identifier: %w", err)
80
80
+
}
81
81
+
82
82
+
client, err := c.withIdentifier(ctx, ri)
83
83
+
if err != nil {
84
84
+
return nil, fmt.Errorf("failed to get client with identifier: %w", err)
85
85
+
}
86
86
+
87
87
+
resp, err := agnostic.RepoGetRecord(ctx, client, "", collection, repo, rkey)
88
88
+
if err != nil {
89
89
+
return nil, fmt.Errorf("failed to get record: %w", err)
90
90
+
}
91
91
+
return resp, nil
92
92
+
}
+63
go.mod
···
1
1
+
module github.com/treethought/goatie
2
2
+
3
3
+
go 1.25.1
4
4
+
5
5
+
require (
6
6
+
github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2
7
7
+
github.com/charmbracelet/bubbles v1.0.0
8
8
+
github.com/charmbracelet/bubbletea v1.3.10
9
9
+
github.com/charmbracelet/lipgloss v1.1.0
10
10
+
github.com/sirupsen/logrus v1.9.4
11
11
+
)
12
12
+
13
13
+
require (
14
14
+
github.com/atotto/clipboard v0.1.4 // indirect
15
15
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
16
16
+
github.com/beorn7/perks v1.0.1 // indirect
17
17
+
github.com/cespare/xxhash/v2 v2.2.0 // indirect
18
18
+
github.com/charmbracelet/colorprofile v0.4.1 // indirect
19
19
+
github.com/charmbracelet/x/ansi v0.11.6 // indirect
20
20
+
github.com/charmbracelet/x/cellbuf v0.0.15 // indirect
21
21
+
github.com/charmbracelet/x/term v0.2.2 // indirect
22
22
+
github.com/clipperhouse/displaywidth v0.9.0 // indirect
23
23
+
github.com/clipperhouse/stringish v0.1.1 // indirect
24
24
+
github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
25
25
+
github.com/earthboundkid/versioninfo/v2 v2.24.1 // indirect
26
26
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
27
27
+
github.com/google/go-cmp v0.6.0 // indirect
28
28
+
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
29
29
+
github.com/ipfs/go-cid v0.4.1 // indirect
30
30
+
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
31
31
+
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
32
32
+
github.com/mattn/go-isatty v0.0.20 // indirect
33
33
+
github.com/mattn/go-localereader v0.0.1 // indirect
34
34
+
github.com/mattn/go-runewidth v0.0.19 // indirect
35
35
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect
36
36
+
github.com/minio/sha256-simd v1.0.1 // indirect
37
37
+
github.com/mr-tron/base58 v1.2.0 // indirect
38
38
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
39
39
+
github.com/muesli/cancelreader v0.2.2 // indirect
40
40
+
github.com/muesli/termenv v0.16.0 // indirect
41
41
+
github.com/multiformats/go-base32 v0.1.0 // indirect
42
42
+
github.com/multiformats/go-base36 v0.2.0 // indirect
43
43
+
github.com/multiformats/go-multibase v0.2.0 // indirect
44
44
+
github.com/multiformats/go-multihash v0.2.3 // indirect
45
45
+
github.com/multiformats/go-varint v0.0.7 // indirect
46
46
+
github.com/prometheus/client_golang v1.17.0 // indirect
47
47
+
github.com/prometheus/client_model v0.5.0 // indirect
48
48
+
github.com/prometheus/common v0.45.0 // indirect
49
49
+
github.com/prometheus/procfs v0.12.0 // indirect
50
50
+
github.com/rivo/uniseg v0.4.7 // indirect
51
51
+
github.com/spaolacci/murmur3 v1.1.0 // indirect
52
52
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect
53
53
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
54
54
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b // indirect
55
55
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 // indirect
56
56
+
golang.org/x/crypto v0.21.0 // indirect
57
57
+
golang.org/x/sys v0.38.0 // indirect
58
58
+
golang.org/x/text v0.14.0 // indirect
59
59
+
golang.org/x/time v0.3.0 // indirect
60
60
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
61
61
+
google.golang.org/protobuf v1.33.0 // indirect
62
62
+
lukechampine.com/blake3 v1.2.1 // indirect
63
63
+
)
+121
go.sum
···
1
1
+
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
2
2
+
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
3
3
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
4
4
+
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
5
5
+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
6
6
+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
7
7
+
github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2 h1:q/dijVJ+cA17e2qmJZPNuB7anByq1W6+uYJr1D9gfto=
8
8
+
github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2/go.mod h1:VG/LeqLGNI3Ew7lsYixajnZGFfWPv144qbUddh+Oyag=
9
9
+
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
10
10
+
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
11
11
+
github.com/charmbracelet/bubbles v1.0.0 h1:12J8/ak/uCZEMQ6KU7pcfwceyjLlWsDLAxB5fXonfvc=
12
12
+
github.com/charmbracelet/bubbles v1.0.0/go.mod h1:9d/Zd5GdnauMI5ivUIVisuEm3ave1XwXtD1ckyV6r3E=
13
13
+
github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw=
14
14
+
github.com/charmbracelet/bubbletea v1.3.10/go.mod h1:ORQfo0fk8U+po9VaNvnV95UPWA1BitP1E0N6xJPlHr4=
15
15
+
github.com/charmbracelet/colorprofile v0.4.1 h1:a1lO03qTrSIRaK8c3JRxJDZOvhvIeSco3ej+ngLk1kk=
16
16
+
github.com/charmbracelet/colorprofile v0.4.1/go.mod h1:U1d9Dljmdf9DLegaJ0nGZNJvoXAhayhmidOdcBwAvKk=
17
17
+
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
18
18
+
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
19
19
+
github.com/charmbracelet/x/ansi v0.11.6 h1:GhV21SiDz/45W9AnV2R61xZMRri5NlLnl6CVF7ihZW8=
20
20
+
github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ=
21
21
+
github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI=
22
22
+
github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q=
23
23
+
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
24
24
+
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
25
25
+
github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA=
26
26
+
github.com/clipperhouse/displaywidth v0.9.0/go.mod h1:aCAAqTlh4GIVkhQnJpbL0T/WfcrJXHcj8C0yjYcjOZA=
27
27
+
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
28
28
+
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
29
29
+
github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
30
30
+
github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
31
31
+
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
32
32
+
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
33
33
+
github.com/earthboundkid/versioninfo/v2 v2.24.1 h1:SJTMHaoUx3GzjjnUO1QzP3ZXK6Ee/nbWyCm58eY3oUg=
34
34
+
github.com/earthboundkid/versioninfo/v2 v2.24.1/go.mod h1:VcWEooDEuyUJnMfbdTh0uFN4cfEIg+kHMuWB2CDCLjw=
35
35
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
36
36
+
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
37
37
+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
38
38
+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
39
39
+
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
40
40
+
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
41
41
+
github.com/ipfs/go-cid v0.4.1 h1:A/T3qGvxi4kpKWWcPC/PgbvDA2bjVLO7n4UeVwnbs/s=
42
42
+
github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk=
43
43
+
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
44
44
+
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
45
45
+
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
46
46
+
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
47
47
+
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
48
48
+
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
49
49
+
github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2JC/oIi4=
50
50
+
github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88=
51
51
+
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
52
52
+
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
53
53
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg=
54
54
+
github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k=
55
55
+
github.com/minio/sha256-simd v1.0.1 h1:6kaan5IFmwTNynnKKpDHe6FWHohJOHhCPchzK49dzMM=
56
56
+
github.com/minio/sha256-simd v1.0.1/go.mod h1:Pz6AKMiUdngCLpeTL/RJY1M9rUuPMYujV5xJjtbRSN8=
57
57
+
github.com/mr-tron/base58 v1.2.0 h1:T/HDJBh4ZCPbU39/+c3rRvE0uKBQlU27+QI8LJ4t64o=
58
58
+
github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc=
59
59
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI=
60
60
+
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo=
61
61
+
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
62
62
+
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
63
63
+
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
64
64
+
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
65
65
+
github.com/multiformats/go-base32 v0.1.0 h1:pVx9xoSPqEIQG8o+UbAe7DNi51oej1NtK+aGkbLYxPE=
66
66
+
github.com/multiformats/go-base32 v0.1.0/go.mod h1:Kj3tFY6zNr+ABYMqeUNeGvkIC/UYgtWibDcT0rExnbI=
67
67
+
github.com/multiformats/go-base36 v0.2.0 h1:lFsAbNOGeKtuKozrtBsAkSVhv1p9D0/qedU9rQyccr0=
68
68
+
github.com/multiformats/go-base36 v0.2.0/go.mod h1:qvnKE++v+2MWCfePClUEjE78Z7P2a1UV0xHgWc0hkp4=
69
69
+
github.com/multiformats/go-multibase v0.2.0 h1:isdYCVLvksgWlMW9OZRYJEa9pZETFivncJHmHnnd87g=
70
70
+
github.com/multiformats/go-multibase v0.2.0/go.mod h1:bFBZX4lKCA/2lyOFSAoKH5SS6oPyjtnzK/XTFDPkNuk=
71
71
+
github.com/multiformats/go-multihash v0.2.3 h1:7Lyc8XfX/IY2jWb/gI7JP+o7JEq9hOa7BFvVU9RSh+U=
72
72
+
github.com/multiformats/go-multihash v0.2.3/go.mod h1:dXgKXCXjBzdscBLk9JkjINiEsCKRVch90MdaGiKsvSM=
73
73
+
github.com/multiformats/go-varint v0.0.7 h1:sWSGR+f/eu5ABZA2ZpYKBILXTTs9JWpdEM/nEGOHFS8=
74
74
+
github.com/multiformats/go-varint v0.0.7/go.mod h1:r8PUYw/fD/SjBCiKOoDlGF6QawOELpZAu9eioSos/OU=
75
75
+
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
76
76
+
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
77
77
+
github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q=
78
78
+
github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY=
79
79
+
github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw=
80
80
+
github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI=
81
81
+
github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM=
82
82
+
github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY=
83
83
+
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
84
84
+
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
85
85
+
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
86
86
+
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
87
87
+
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
88
88
+
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
89
89
+
github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
90
90
+
github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
91
91
+
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
92
92
+
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
93
93
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e h1:28X54ciEwwUxyHn9yrZfl5ojgF4CBNLWX7LR0rvBkf4=
94
94
+
github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e/go.mod h1:pM99HXyEbSQHcosHc0iW7YFmwnscr+t9Te4ibko05so=
95
95
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
96
96
+
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
97
97
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b h1:CzigHMRySiX3drau9C6Q5CAbNIApmLdat5jPMqChvDA=
98
98
+
gitlab.com/yawning/secp256k1-voi v0.0.0-20230925100816-f2616030848b/go.mod h1:/y/V339mxv2sZmYYR64O07VuCpdNZqCTwO8ZcouTMI8=
99
99
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02 h1:qwDnMxjkyLmAFgcfgTnfJrmYKWhHnci3GjDqcZp1M3Q=
100
100
+
gitlab.com/yawning/tuplehash v0.0.0-20230713102510-df83abbf9a02/go.mod h1:JTnUj0mpYiAsuZLmKjTx/ex3AtMowcCgnE7YNyCEP0I=
101
101
+
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
102
102
+
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
103
103
+
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
104
104
+
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
105
105
+
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
106
106
+
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
107
107
+
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
108
108
+
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
109
109
+
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
110
110
+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
111
111
+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
112
112
+
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
113
113
+
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
114
114
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 h1:+cNy6SZtPcJQH3LJVLOSmiC7MMxXNOb3PU/VUEz+EhU=
115
115
+
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90=
116
116
+
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
117
117
+
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
118
118
+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
119
119
+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
120
120
+
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
121
121
+
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
+36
main.go
···
1
1
+
package main
2
2
+
3
3
+
import (
4
4
+
"os"
5
5
+
6
6
+
log "github.com/sirupsen/logrus"
7
7
+
"github.com/treethought/goatie/ui"
8
8
+
9
9
+
"github.com/bluesky-social/indigo/atproto/identity"
10
10
+
tea "github.com/charmbracelet/bubbletea"
11
11
+
)
12
12
+
13
13
+
type Client struct {
14
14
+
dir identity.Directory
15
15
+
}
16
16
+
17
17
+
func main() {
18
18
+
log.SetFormatter(&log.TextFormatter{
19
19
+
FullTimestamp: false,
20
20
+
DisableTimestamp: true,
21
21
+
})
22
22
+
f, err := os.Create("debug.log")
23
23
+
if err != nil {
24
24
+
log.Fatal(err)
25
25
+
}
26
26
+
defer f.Close()
27
27
+
log.SetOutput(f)
28
28
+
log.Warn("starting goatie")
29
29
+
30
30
+
app := ui.NewApp()
31
31
+
32
32
+
p := tea.NewProgram(app)
33
33
+
if _, err := p.Run(); err != nil {
34
34
+
log.Fatal(err)
35
35
+
}
36
36
+
}
+178
ui/app.go
···
1
1
+
package ui
2
2
+
3
3
+
import (
4
4
+
"context"
5
5
+
"fmt"
6
6
+
7
7
+
log "github.com/sirupsen/logrus"
8
8
+
9
9
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
10
10
+
"github.com/bluesky-social/indigo/atproto/syntax"
11
11
+
"github.com/charmbracelet/bubbles/spinner"
12
12
+
"github.com/charmbracelet/bubbles/textinput"
13
13
+
tea "github.com/charmbracelet/bubbletea"
14
14
+
"github.com/treethought/goatie/at"
15
15
+
)
16
16
+
17
17
+
type App struct {
18
18
+
client *at.Client
19
19
+
search *CommandPallete
20
20
+
repoView *RepoView
21
21
+
active tea.Model
22
22
+
err string
23
23
+
}
24
24
+
25
25
+
func NewApp() *App {
26
26
+
search := &CommandPallete{}
27
27
+
repoView := &RepoView{}
28
28
+
return &App{
29
29
+
client: at.NewClient(""),
30
30
+
search: search,
31
31
+
repoView: repoView,
32
32
+
active: search,
33
33
+
}
34
34
+
}
35
35
+
36
36
+
func (a *App) Init() tea.Cmd {
37
37
+
return a.active.Init()
38
38
+
}
39
39
+
40
40
+
func (a *App) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
41
41
+
switch msg := msg.(type) {
42
42
+
// top level always handle ctrl-c
43
43
+
case tea.KeyMsg:
44
44
+
switch msg.String() {
45
45
+
case "ctrl+c", "q":
46
46
+
return a, tea.Quit
47
47
+
case "esc":
48
48
+
// Go back to search from repo view
49
49
+
if a.active == a.repoView {
50
50
+
a.active = a.search
51
51
+
a.search.loading = false
52
52
+
return a, nil
53
53
+
}
54
54
+
}
55
55
+
56
56
+
case searchSubmitMsg:
57
57
+
// parse for handle/DID or a record URI
58
58
+
id, err := syntax.ParseAtIdentifier(msg.identifier.String())
59
59
+
if err != nil {
60
60
+
log.Fatalf("Failed to parse identifier, should have caught during submission: %s", err.Error())
61
61
+
return a, nil
62
62
+
}
63
63
+
if id.IsDID() || id.IsHandle() {
64
64
+
log.Printf("Repo identifier submitted: %s", id.String())
65
65
+
return a, a.fetchRepo(id.String())
66
66
+
}
67
67
+
68
68
+
case repoLoadedMsg:
69
69
+
a.repoView.SetRepo(msg.repo)
70
70
+
a.active = a.repoView
71
71
+
a.search.loading = false
72
72
+
return a, nil
73
73
+
74
74
+
case repoErrorMsg:
75
75
+
a.search.err = msg.err.Error()
76
76
+
a.search.loading = false
77
77
+
return a, nil
78
78
+
}
79
79
+
80
80
+
var cmd tea.Cmd
81
81
+
a.active, cmd = a.active.Update(msg)
82
82
+
return a, cmd
83
83
+
}
84
84
+
85
85
+
func (a *App) fetchRepo(repoId string) tea.Cmd {
86
86
+
return func() tea.Msg {
87
87
+
resp, err := a.client.GetRepo(context.Background(), repoId)
88
88
+
if err != nil {
89
89
+
log.Printf("Failed to get repo: %s", err.Error())
90
90
+
return repoErrorMsg{err: err}
91
91
+
}
92
92
+
log.WithFields(log.Fields{
93
93
+
"repo": resp.Handle,
94
94
+
}).Info("Repo loaded")
95
95
+
return repoLoadedMsg{repo: resp}
96
96
+
}
97
97
+
}
98
98
+
99
99
+
func (a *App) View() string {
100
100
+
return a.active.View()
101
101
+
}
102
102
+
103
103
+
// Message types
104
104
+
type searchSubmitMsg struct {
105
105
+
identifier syntax.AtIdentifier
106
106
+
}
107
107
+
108
108
+
type repoLoadedMsg struct {
109
109
+
repo *comatproto.RepoDescribeRepo_Output
110
110
+
}
111
111
+
112
112
+
type repoErrorMsg struct {
113
113
+
err error
114
114
+
}
115
115
+
116
116
+
type CommandPallete struct {
117
117
+
ti textinput.Model
118
118
+
err string
119
119
+
loading bool
120
120
+
spinner spinner.Model
121
121
+
}
122
122
+
123
123
+
func (c *CommandPallete) Init() tea.Cmd {
124
124
+
c.ti = textinput.New()
125
125
+
c.ti.Placeholder = "Enter handle, DID, or AT URI"
126
126
+
c.ti.Focus()
127
127
+
c.spinner = spinner.New()
128
128
+
c.spinner.Spinner = spinner.Dot
129
129
+
return textinput.Blink
130
130
+
}
131
131
+
132
132
+
func (c *CommandPallete) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
133
133
+
134
134
+
switch msg := msg.(type) {
135
135
+
case tea.KeyMsg:
136
136
+
switch msg.String() {
137
137
+
case "enter":
138
138
+
val := c.ti.Value()
139
139
+
if val == "" {
140
140
+
c.err = "Input cannot be empty"
141
141
+
return c, nil
142
142
+
}
143
143
+
id, err := syntax.ParseAtIdentifier(val)
144
144
+
if err != nil {
145
145
+
c.err = fmt.Sprintf("Must use handle, DID or AT URI: %s", err.Error())
146
146
+
return c, nil
147
147
+
}
148
148
+
c.err = ""
149
149
+
c.loading = true
150
150
+
return c, func() tea.Msg {
151
151
+
log.Printf("Looking up identifier: %s", id.String())
152
152
+
return searchSubmitMsg{identifier: id}
153
153
+
}
154
154
+
}
155
155
+
156
156
+
}
157
157
+
158
158
+
var cmds []tea.Cmd
159
159
+
ti, tcmd := c.ti.Update(msg)
160
160
+
c.ti = ti
161
161
+
cmds = append(cmds, tcmd)
162
162
+
163
163
+
sp, scmd := c.spinner.Update(msg)
164
164
+
c.spinner = sp
165
165
+
cmds = append(cmds, scmd)
166
166
+
167
167
+
return c, tea.Batch(cmds...)
168
168
+
}
169
169
+
170
170
+
func (c *CommandPallete) View() string {
171
171
+
s := fmt.Sprint("Search:\n", c.ti.View())
172
172
+
if c.err != "" {
173
173
+
s += fmt.Sprintf("\nError: %s", c.err)
174
174
+
} else if c.loading {
175
175
+
s += "\nLoading... " + c.spinner.View()
176
176
+
}
177
177
+
return s
178
178
+
}
+90
ui/repo.go
···
1
1
+
package ui
2
2
+
3
3
+
import (
4
4
+
"fmt"
5
5
+
comatproto "github.com/bluesky-social/indigo/api/atproto"
6
6
+
tea "github.com/charmbracelet/bubbletea"
7
7
+
"github.com/charmbracelet/lipgloss"
8
8
+
"strings"
9
9
+
)
10
10
+
11
11
+
// =============================================================================
12
12
+
// RepoView - Displays repository information and collections
13
13
+
// =============================================================================
14
14
+
15
15
+
var (
16
16
+
headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205"))
17
17
+
labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241"))
18
18
+
valueStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("255"))
19
19
+
collectionStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("42"))
20
20
+
dimStyle = lipgloss.NewStyle().Faint(true)
21
21
+
)
22
22
+
23
23
+
type RepoView struct {
24
24
+
repo *comatproto.RepoDescribeRepo_Output
25
25
+
}
26
26
+
27
27
+
func (r *RepoView) Init() tea.Cmd {
28
28
+
return nil
29
29
+
}
30
30
+
31
31
+
func (r *RepoView) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
32
32
+
// RepoView doesn't handle messages directly
33
33
+
// Parent App handles navigation
34
34
+
return r, nil
35
35
+
}
36
36
+
37
37
+
func (r *RepoView) View() string {
38
38
+
if r.repo == nil {
39
39
+
return "No repository loaded"
40
40
+
}
41
41
+
42
42
+
var s strings.Builder
43
43
+
44
44
+
// Header
45
45
+
s.WriteString(headerStyle.Render("📦 Repository Information"))
46
46
+
s.WriteString("\n\n")
47
47
+
48
48
+
// Repository details
49
49
+
s.WriteString(labelStyle.Render("Handle: "))
50
50
+
s.WriteString(valueStyle.Render(r.repo.Handle))
51
51
+
s.WriteString("\n")
52
52
+
53
53
+
s.WriteString(labelStyle.Render("DID: "))
54
54
+
s.WriteString(valueStyle.Render(r.repo.Did))
55
55
+
s.WriteString("\n")
56
56
+
57
57
+
if r.repo.DidDoc != nil {
58
58
+
s.WriteString(labelStyle.Render("DID Document: "))
59
59
+
s.WriteString(dimStyle.Render("Available"))
60
60
+
s.WriteString("\n")
61
61
+
}
62
62
+
63
63
+
s.WriteString("\n")
64
64
+
65
65
+
// Collections section
66
66
+
s.WriteString(headerStyle.Render("Collections"))
67
67
+
s.WriteString(fmt.Sprintf(" (%d total)", len(r.repo.Collections)))
68
68
+
s.WriteString("\n\n")
69
69
+
70
70
+
if len(r.repo.Collections) == 0 {
71
71
+
s.WriteString(dimStyle.Render("No collections found"))
72
72
+
} else {
73
73
+
for _, collection := range r.repo.Collections {
74
74
+
s.WriteString(" • ")
75
75
+
s.WriteString(collectionStyle.Render(collection))
76
76
+
s.WriteString("\n")
77
77
+
}
78
78
+
}
79
79
+
80
80
+
s.WriteString("\n\n")
81
81
+
82
82
+
// Footer with help
83
83
+
s.WriteString(dimStyle.Render("Press Esc to go back • Ctrl+C to quit"))
84
84
+
85
85
+
return s.String()
86
86
+
}
87
87
+
88
88
+
func (r *RepoView) SetRepo(repo *comatproto.RepoDescribeRepo_Output) {
89
89
+
r.repo = repo
90
90
+
}