AT Protocol Terminal Interface Explorer

initial commit

Cam Sweeney 6e6f65e8

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