A terminal client for Tangled.
tui go tangled cli
8

Configure Feed

Select the types of activity you want to include in your feed.

pagination: add fetchAllPages helper and use in all list functions

Aly Raffauf (Jul 10, 2026, 4:21 PM EDT) 451de824 a85428fb

+105 -17
+29
atproto/records.go
··· 45 45 Reverse bool 46 46 } 47 47 48 + // maxRecordPages caps how many pages ListAllRecords will follow, as a 49 + // safety net against a server that never returns an empty cursor. 50 + const maxRecordPages = 1000 51 + 48 52 // PutRecord writes a record to the PDS, returning its at:// URI and CID. 49 53 func (a *ATProto) PutRecord(ctx context.Context, in PutRecordInput) (uri, cid string, err error) { 50 54 var out struct { ··· 90 94 } 91 95 return &out, nil 92 96 } 97 + 98 + // ListAllRecords fetches every record in collection for repo, following 99 + // pagination cursors until the listing is exhausted. opts.Limit sets the 100 + // page size; opts.Cursor is ignored since pagination always starts from 101 + // the first page. 102 + func (a *ATProto) ListAllRecords(ctx context.Context, repo, collection string, opts ListRecordsOpts) ([]RecordItem, error) { 103 + var all []RecordItem 104 + cursor := "" 105 + 106 + for range maxRecordPages { 107 + opts.Cursor = cursor 108 + out, err := a.ListRecords(ctx, repo, collection, opts) 109 + if err != nil { 110 + return nil, err 111 + } 112 + all = append(all, out.Records...) 113 + 114 + if out.Cursor == nil || *out.Cursor == "" { 115 + return all, nil 116 + } 117 + cursor = *out.Cursor 118 + } 119 + 120 + return nil, fmt.Errorf("exceeded %d pages listing %s records for %q", maxRecordPages, collection, repo) 121 + }
+2 -2
internal/cli/ssh_key_list.go
··· 36 36 } 37 37 38 38 atClient := &atproto.ATProto{Client: &atclient.APIClient{Host: pdsURL}} 39 - out, err := atClient.ListRecords(ctx, ident.DID.String(), "sh.tangled.publicKey", atproto.ListRecordsOpts{Limit: defaultListLimit}) 39 + records, err := atClient.ListAllRecords(ctx, ident.DID.String(), "sh.tangled.publicKey", atproto.ListRecordsOpts{Limit: defaultListLimit}) 40 40 if err != nil { 41 41 return fmt.Errorf("list SSH keys for %q: %w", handle, err) 42 42 } 43 43 44 - items := buildSSHKeyItems(out.Records) 44 + items := buildSSHKeyItems(records) 45 45 return output(items, renderSSHKeyList) 46 46 }, 47 47 }
+38 -3
tangled/list.go
··· 1 1 package tangled 2 2 3 - import "encoding/json" 3 + import ( 4 + "context" 5 + "encoding/json" 6 + "fmt" 7 + ) 8 + 9 + // maxPaginationPages caps how many pages fetchAllPages will follow, as a 10 + // safety net against a server that never returns an empty cursor. 11 + const maxPaginationPages = 1000 4 12 5 13 // ListItem is one item in an issue or pull-request listing. 6 14 type ListItem struct { ··· 26 34 Order string // "asc" or "desc" 27 35 } 28 36 29 - // params builds the XRPC query parameters for subject. 30 - func (o ListOpts) params(subject string) map[string]any { 37 + // params builds the XRPC query parameters for subject, requesting the page 38 + // after cursor (the first page if cursor is empty). 39 + func (o ListOpts) params(subject, cursor string) map[string]any { 31 40 params := map[string]any{"subject": subject} 32 41 if o.Author != "" { 33 42 params["author"] = o.Author ··· 43 52 if o.Order != "" { 44 53 params["order"] = o.Order 45 54 } 55 + if cursor != "" { 56 + params["cursor"] = cursor 57 + } 46 58 return params 47 59 } 60 + 61 + // fetchAllPages calls fetch for successive pages, advancing the cursor it 62 + // returns, until a page reports no further cursor. It returns every item 63 + // across all pages combined. 64 + func fetchAllPages[T any](ctx context.Context, fetch func(ctx context.Context, cursor string) (items []T, nextCursor *string, err error)) ([]T, error) { 65 + var all []T 66 + cursor := "" 67 + 68 + for page := 0; page < maxPaginationPages; page++ { 69 + items, nextCursor, err := fetch(ctx, cursor) 70 + if err != nil { 71 + return nil, err 72 + } 73 + all = append(all, items...) 74 + 75 + if nextCursor == nil || *nextCursor == "" { 76 + return all, nil 77 + } 78 + cursor = *nextCursor 79 + } 80 + 81 + return nil, fmt.Errorf("exceeded %d pages without reaching the end of the list", maxPaginationPages) 82 + }
+11 -3
tangled/list_issues.go
··· 17 17 References []string `json:"references,omitempty"` 18 18 } 19 19 20 + // ListIssues fetches every issue for repoDid, following pagination 21 + // cursors until the listing is exhausted. 20 22 func (t *Tangled) ListIssues(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { 21 - var out List 22 - if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listIssues"), opts.params(repoDid), &out); err != nil { 23 + items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]ListItem, *string, error) { 24 + var page List 25 + if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listIssues"), opts.params(repoDid, cursor), &page); err != nil { 26 + return nil, nil, err 27 + } 28 + return page.Items, page.Cursor, nil 29 + }) 30 + if err != nil { 23 31 return nil, fmt.Errorf("list issues for %q: %w", repoDid, err) 24 32 } 25 - return &out, nil 33 + return &List{Items: items}, nil 26 34 }
+11 -3
tangled/list_pulls.go
··· 44 44 Size int64 `json:"size"` 45 45 } 46 46 47 + // ListPulls fetches every pull request for repoDid, following pagination 48 + // cursors until the listing is exhausted. 47 49 func (t *Tangled) ListPulls(ctx context.Context, repoDid string, opts ListOpts) (*List, error) { 48 - var out List 49 - if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listPulls"), opts.params(repoDid), &out); err != nil { 50 + items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]ListItem, *string, error) { 51 + var page List 52 + if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listPulls"), opts.params(repoDid, cursor), &page); err != nil { 53 + return nil, nil, err 54 + } 55 + return page.Items, page.Cursor, nil 56 + }) 57 + if err != nil { 50 58 return nil, fmt.Errorf("list PRs for %q: %w", repoDid, err) 51 59 } 52 - return &out, nil 60 + return &List{Items: items}, nil 53 61 }
+14 -6
tangled/list_repos.go
··· 12 12 Cursor *string `json:"cursor"` 13 13 } 14 14 15 + // ListRepos fetches every repo owned by ownerDid, following pagination 16 + // cursors until the listing is exhausted. 15 17 func (t *Tangled) ListRepos(ctx context.Context, ownerDid string) (*RepoList, error) { 16 - var repos RepoList 17 - err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listRepos"), map[string]any{ 18 - "subject": ownerDid, 19 - "limit": 100, 20 - }, &repos) 18 + items, err := fetchAllPages(ctx, func(ctx context.Context, cursor string) ([]Repo, *string, error) { 19 + params := map[string]any{"subject": ownerDid, "limit": 100} 20 + if cursor != "" { 21 + params["cursor"] = cursor 22 + } 23 + var page RepoList 24 + if err := t.Client.Get(ctx, syntax.NSID("sh.tangled.repo.listRepos"), params, &page); err != nil { 25 + return nil, nil, err 26 + } 27 + return page.Items, page.Cursor, nil 28 + }) 21 29 if err != nil { 22 30 return nil, fmt.Errorf("list tangled repos for %q: %w", ownerDid, err) 23 31 } 24 32 25 - return &repos, nil 33 + return &RepoList{Items: items}, nil 26 34 }