AT Protocol Terminal Interface Explorer

use list bubbl for collections

+153 -49
+1 -1
at/client.go
··· 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 {
··· 57 return nil, fmt.Errorf("failed to get client with identifier: %w", err) 58 } 59 60 + // TODO: download 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 {
+1
go.mod
··· 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
··· 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/sahilm/fuzzy v0.1.1 // indirect 52 github.com/spaolacci/murmur3 v1.1.0 // indirect 53 github.com/whyrusleeping/cbor-gen v0.2.1-0.20241030202151-b7a6831be65e // indirect 54 github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
+8
go.sum
··· 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= ··· 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= ··· 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= ··· 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=
··· 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/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY= 6 + github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E= 7 github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 8 github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 9 github.com/bluesky-social/indigo v0.0.0-20260213232405-1286ca7a7cb2 h1:q/dijVJ+cA17e2qmJZPNuB7anByq1W6+uYJr1D9gfto= ··· 22 github.com/charmbracelet/x/ansi v0.11.6/go.mod h1:2JNYLgQUsyqaiLovhU2Rv/pb8r6ydXKS3NIttu3VGZQ= 23 github.com/charmbracelet/x/cellbuf v0.0.15 h1:ur3pZy0o6z/R7EylET877CBxaiE1Sp1GMxoFPAIztPI= 24 github.com/charmbracelet/x/cellbuf v0.0.15/go.mod h1:J1YVbR7MUuEGIFPCaaZ96KDl5NoS0DAWkskup+mOY+Q= 25 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91 h1:payRxjMjKgx2PaCWLZ4p3ro9y97+TVLZNaRZgJwSVDQ= 26 + github.com/charmbracelet/x/exp/golden v0.0.0-20241011142426-46044092ad91/go.mod h1:wDlXFlCrmJ8J+swcL/MnGUuYnqgQdW9rhSD61oNMb6U= 27 github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk= 28 github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI= 29 github.com/clipperhouse/displaywidth v0.9.0 h1:Qb4KOhYwRiN3viMv1v/3cTBlz3AcAZX3+y9OLhMtAtA= ··· 46 github.com/ipfs/go-cid v0.4.1/go.mod h1:uQHwDeX4c6CtyrFwdqyhpNcxVewur1M7l7fNU7LKwZk= 47 github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 48 github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 49 + github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 50 + github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 51 github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= 52 github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= 53 github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= ··· 90 github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= 91 github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 92 github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 93 + github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= 94 + github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= 95 github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w= 96 github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g= 97 github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI=
+2 -2
main.go
··· 16 17 func main() { 18 log.SetFormatter(&log.TextFormatter{ 19 - FullTimestamp: false, 20 DisableTimestamp: true, 21 }) 22 f, err := os.Create("debug.log") ··· 29 30 app := ui.NewApp() 31 32 - p := tea.NewProgram(app) 33 if _, err := p.Run(); err != nil { 34 log.Fatal(err) 35 }
··· 16 17 func main() { 18 log.SetFormatter(&log.TextFormatter{ 19 + FullTimestamp: false, 20 DisableTimestamp: true, 21 }) 22 f, err := os.Create("debug.log") ··· 29 30 app := ui.NewApp() 31 32 + p := tea.NewProgram(app, tea.WithAltScreen()) 33 if _, err := p.Run(); err != nil { 34 log.Fatal(err) 35 }
+6 -3
ui/app.go
··· 24 25 func NewApp() *App { 26 search := &CommandPallete{} 27 - repoView := &RepoView{} 28 return &App{ 29 client: at.NewClient(""), 30 search: search, ··· 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": ··· 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()
··· 24 25 func NewApp() *App { 26 search := &CommandPallete{} 27 + repoView := NewRepoView() 28 return &App{ 29 client: at.NewClient(""), 30 search: search, ··· 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.WindowSizeMsg: 44 + a.active, _ = a.active.Update(msg) 45 + return a, nil 46 case tea.KeyMsg: 47 switch msg.String() { 48 case "ctrl+c", "q": ··· 69 } 70 71 case repoLoadedMsg: 72 + cmd := a.repoView.SetRepo(msg.repo) 73 a.active = a.repoView 74 a.search.loading = false 75 + return a, cmd 76 77 case repoErrorMsg: 78 a.search.err = msg.err.Error()
+135 -43
ui/repo.go
··· 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")) ··· 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 }
··· 2 3 import ( 4 "fmt" 5 + "strings" 6 + 7 comatproto "github.com/bluesky-social/indigo/api/atproto" 8 + "github.com/charmbracelet/bubbles/list" 9 tea "github.com/charmbracelet/bubbletea" 10 "github.com/charmbracelet/lipgloss" 11 ) 12 13 var ( 14 headerStyle = lipgloss.NewStyle().Bold(true).Foreground(lipgloss.Color("205")) 15 labelStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("241")) ··· 18 dimStyle = lipgloss.NewStyle().Faint(true) 19 ) 20 21 + type CollectionList struct { 22 + list list.Model 23 + } 24 + 25 + type CollectionListItem struct { 26 + Name string 27 + } 28 + 29 + func (c CollectionListItem) FilterValue() string { 30 + return c.Name 31 + } 32 + func (c CollectionListItem) Title() string { 33 + return collectionStyle.Render(c.Name) 34 + } 35 + func (c CollectionListItem) Description() string { 36 + return "" 37 + } 38 + 39 + func NewCollectionList(collections []string) *CollectionList { 40 + items := make([]list.Item, len(collections)) 41 + for i, col := range collections { 42 + ci := CollectionListItem{Name: col} 43 + items[i] = list.Item(ci) 44 + } 45 + del := list.DefaultDelegate{ 46 + ShowDescription: false, 47 + Styles: list.NewDefaultItemStyles(), 48 + } 49 + del.SetHeight(1) 50 + 51 + l := list.New(items, del, 80, 20) 52 + l.SetShowTitle(false) 53 + l.SetShowStatusBar(false) 54 + l.SetFilteringEnabled(false) 55 + return &CollectionList{ 56 + list: l, 57 + } 58 } 59 60 + func (cl *CollectionList) Init() tea.Cmd { 61 return nil 62 } 63 64 + func (cl *CollectionList) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 65 + var cmd tea.Cmd 66 + cl.list, cmd = cl.list.Update(msg) 67 + return cl, cmd 68 } 69 70 + func (cl *CollectionList) View() string { 71 + if len(cl.list.Items()) == 0 { 72 + return dimStyle.Render("No collections found") 73 } 74 + return cl.list.View() 75 + } 76 77 + type RepoView struct { 78 + repo *comatproto.RepoDescribeRepo_Output 79 + clist *CollectionList 80 + header string 81 + width int 82 + height int 83 + } 84 + 85 + func NewRepoView() *RepoView { 86 + return &RepoView{ 87 + clist: NewCollectionList([]string{}), 88 + width: 80, 89 + height: 24, 90 + } 91 + } 92 + 93 + func (r *RepoView) buildHeader() string { 94 + if r.repo == nil { 95 + return "" 96 + } 97 var s strings.Builder 98 99 + // Title 100 + s.WriteString(headerStyle.Render("📦 Repository")) 101 s.WriteString("\n\n") 102 103 s.WriteString(labelStyle.Render("Handle: ")) 104 s.WriteString(valueStyle.Render(r.repo.Handle)) 105 + s.WriteString(" ") 106 + s.WriteString(labelStyle.Render("Valid: ")) 107 + if r.repo.HandleIsCorrect { 108 + s.WriteString(valueStyle.Render("✓")) 109 + } else { 110 + s.WriteString(dimStyle.Render("✗")) 111 + } 112 s.WriteString("\n") 113 114 s.WriteString(labelStyle.Render("DID: ")) 115 + s.WriteString(dimStyle.Render(r.repo.Did)) 116 + s.WriteString("\n\n") 117 + 118 + // Collections section header 119 + s.WriteString(headerStyle.Render("Collections ")) 120 + s.WriteString(dimStyle.Render(fmt.Sprintf("(%d)", len(r.repo.Collections)))) 121 s.WriteString("\n") 122 123 + return s.String() 124 + } 125 126 + func (r *RepoView) SetRepo(repo *comatproto.RepoDescribeRepo_Output) tea.Cmd { 127 + r.repo = repo 128 + r.header = r.buildHeader() 129 + r.clist = NewCollectionList(repo.Collections) 130 + r.updateListSize() 131 + return r.clist.Init() 132 + } 133 134 + func (r *RepoView) Init() tea.Cmd { 135 + return nil 136 + } 137 138 + func (r *RepoView) Update(msg tea.Msg) (tea.Model, tea.Cmd) { 139 + switch msg := msg.(type) { 140 + case tea.WindowSizeMsg: 141 + r.width = msg.Width 142 + r.height = msg.Height 143 + r.updateListSize() 144 } 145 + clist, cmd := r.clist.Update(msg) 146 + r.clist = clist.(*CollectionList) 147 + return r, cmd 148 + } 149 150 + // updateListSize calculates and sets the list size to fill remaining space 151 + func (r *RepoView) updateListSize() { 152 + if r.clist == nil { 153 + return 154 + } 155 + headerHeight := lipgloss.Height(r.header) 156 + footerHeight := 2 // "\n" + help text line 157 158 + // List gets all remaining space 159 + listHeight := r.height - headerHeight - footerHeight 160 + if listHeight < 5 { 161 + listHeight = 5 162 + } 163 164 + r.clist.list.SetSize(r.width, listHeight) 165 } 166 167 + func (r *RepoView) View() string { 168 + if r.repo == nil { 169 + return "No repository loaded" 170 + } 171 + 172 + // Footer help text 173 + footer := dimStyle.Render("Press Esc to go back • ↑/↓ or j/k to navigate • Ctrl+C to quit") 174 + 175 + // Join header (fixed), list (scrollable), and footer 176 + return lipgloss.JoinVertical( 177 + lipgloss.Left, 178 + r.header, 179 + r.clist.View(), 180 + "\n"+footer, 181 + ) 182 }